From e3764e3c370a142ca993df650ad913875b1f5fd8 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Tue, 27 Oct 2026 14:04:30 -0400 Subject: [PATCH] fix(pcap,pcapng): prefix capture-guard messages and name the block type in option guards (#2300, #2301) #2300: the guards that reject a file's magic number or byte order had no PCAP:/PCAP-NG: prefix, unlike every other message in those modules, and PCAP's magic-number guard didn't say what it read. Four sites: schema/misc/pcap/header.py's magic_number_callback, misc/pcap/header.py's _make_magic (byte order unresolved/unknown), and two byte-order sites in schema/misc/pcapng.py that were the only two of nine messages in that file still missing PCAP-NG:. #2301: 128 PCAP-NG option guards in misc/pcapng.py ended in a full stop, unlike the house : ... form (#2232, #2257), and the placement guard formatted self._type -- a plain int at make time, not the Enum_ BlockType it's cast to -- with bare {}, showing a raw number. A new _block_type_name() helper resolves either form through Enum_BlockType, idempotent for the enum case; applying the int form via .name directly would raise AttributeError, verified against pristine main. Also fixed: the if_tsresol/if_tzone option guards carried the identical full-stop defect, outside the issue's literal checklist but left would have made this consistency fix inconsistent with itself -- the file's full-stop count is now 0, which a new test asserts as an invariant. CHANGELOG.md regenerated (util/changelog_md.py) after correcting one docs/source/changelog/1.5.0.rst entry that quoted the old literal message. Tests: new tests/protocols/misc/test_capture_message_prefix_2300_2301_unit.py, 14 cases, 13 fail / 1 passes against pristine main (the one passing case is a characterisation pin of _type's runtime type, true on both trees). --- CHANGELOG.md | 2 +- docs/source/changelog/1.5.0.rst | 5 +- pcapkit/protocols/misc/pcap/header.py | 4 +- pcapkit/protocols/misc/pcapng.py | 283 +++++++++-------- pcapkit/protocols/schema/misc/pcap/header.py | 2 +- pcapkit/protocols/schema/misc/pcapng.py | 4 +- ...t_capture_message_prefix_2300_2301_unit.py | 300 ++++++++++++++++++ ...t_pcapng_raw_option_byteorder_2353_unit.py | 10 +- .../misc/test_pcapng_roundtrip_unit.py | 2 +- .../test_pcapng_short_shb_idb_1422_unit.py | 3 +- 10 files changed, 470 insertions(+), 145 deletions(-) create mode 100644 tests/protocols/misc/test_capture_message_prefix_2300_2301_unit.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 180b2c8df9..0b2c96960e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -266,7 +266,7 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r - every PCAP-NG packet block now carries the captured octets it declares. Every Enhanced Packet Block of the committed `examples/captures/dhcp.pcapng` reported `len(packet) == 0` against a `captured_len` of 314, 342, 314 and 342, as did the Simple Packet Block and obsolete Packet Block (synthesised; no committed fixture has either). Those block types, exactly `PCAPNG.PACKET_TYPES`, are the whole blast radius. The octets were read correctly and thrown away: `PCAPNG.unpack()` extracted them from the block schema, and `ProtocolBase.__init__()` overwrote the result with `self.packet.payload`. The inherited `packet` splits a protocol at `self.length` octets of header and takes the rest as payload, which holds only for a header followed by its payload; `PCAPNG.length` is the *Block Total Length*, so the split consumed the whole block as header (`len(frame.packet.header) == 348` on a 348-octet block), and a block's trailer (option list, repeated total length) means no `length` could make it right. The fix is at the computation rather than the injection: `PCAPNG.packet` is overridden to take the payload from the block schema's `__payload__` field and the header from the octets ahead of it, summed out of the schema buffers so the packet types' payload offsets (28, 12, 28) are not hard-coded. Teaching `ProtocolBase.__init__()` to skip a protocol that had set `packet` was rejected: it heals `frame.info.packet` while `frame.packet` still reports the whole block as header. The override is a plain `property`, not the `cached_property` it replaces: the inherited one caches because it *reads the stream*, where this one only walks buffers already filled, and caching it would let a second `unpack` on one instance return the *first* call's octets. This moves the parse output of every PCAP-NG capture on the success path, so it ships labelled breaking: `frame.info.packet` goes from `b''` to hundreds of octets, `frame.packet.header` from the whole block to the pre-payload prefix. It was also a wire-format defect: `PCAPIO` writes `value.packet` after each record header, so a dump declared hundreds of octets per record and delivered none, desynchronising any reader that walks by `incl_len`. It round-trips now. The uncaught `ValueError` on an EOF-truncated PCAP-NG ([#678](https://github.com/JarryShaw/PyPCAPKit/issues/678)) is untouched, and [#685](https://github.com/JarryShaw/PyPCAPKit/issues/685) (below) later removed the stale `examples/captures/pcapng.txt` from the index ([#646](https://github.com/JarryShaw/PyPCAPKit/issues/646)). - an unpadded HTTP/2 `DATA`, `HEADERS` or `PUSH_PROMISE` frame parsed with its whole payload silently discarded. The payload length callbacks for those frames in `pcapkit/protocols/schema/application/httpv2.py` put the conditional in the wrong place: a conditional binds looser than `-`, so `pkt['__length__'] - pkt['pad_len'] if pkt['flags']['bit_3'] else 0` groups as `(pkt['__length__'] - pkt['pad_len']) if ... else 0`, and the `else` arm returned `0` ("read no octets") where it meant to subtract `0`. Padding is rare in HTTP/2, so the broken arm was the common one, and nothing raised or warned: `info.data` was `b''`. An unpadded `DATA` frame declaring `b'{"ok":true}\n'` now parses as those twelve octets, and unpadded `HEADERS` and `PUSH_PROMISE` keep their header block fragment. Padded frames are unaffected, since `(A - B) if T else 0` and `A - (B if T else 0)` are both `A - B` for truthy `T`; the padded arm has no off-by-one ([#668](https://github.com/JarryShaw/PyPCAPKit/issues/668) asked that be checked), because `Schema.unpack()` decrements `packet['__length__']` by each field's width, so `pad_len`'s own octet is already out by the time the payload field runs. The same lambda also governs **packing**: an unpadded `DATA` frame carrying twelve octets packed to a nine-octet header whose length field declared **21**, body absent, so pcapkit was *emitting* malformed HTTP/2; a `make` -> parse -> `make` cycle of a non-empty unpadded payload now closes byte-for-byte. The only other conditional of this shape, `TSOption.remainder` in `pcapkit/protocols/schema/internet/ipv4.py`, is correct: its `else 0` is intentional because `ts_data` consumes the whole option data area in that arm. The new tests use non-zero padding rather than the zeros [RFC 9113 Section 6.1](https://datatracker.ietf.org/doc/html/rfc9113#section-6.1) tells a sender to use, so failing to subtract it yields a different byte string. Ships labelled breaking: restoring a dropped payload moves the parse output of essentially every HTTP/2 capture, and constructed frames change byte-for-byte, from malformed to correct ([#668](https://github.com/JarryShaw/PyPCAPKit/issues/668)). - the schema half of every option-like registrar overwrote silently while the parser half of the same call warned. `EnumSchema.register()` was a bare `cls.__enum__[code] = schema`, and is the schema half of the public option registrars, so one `register_ipv4_option` call replacing a built-in named the parser it displaced and said nothing about the schema. `EnumSchema.__init_subclass__()` reaches the same registry without calling `register`, so a subclass declared with a `code=` keyword (the documented way to add a schema) stayed silent too; both are guarded, and the declaration hook's branches are folded into one loop. Presence is a faithful test only because `_EnumRegistry.__missing__()` returns a miss without recording it, so parsing an unknown code cannot make the next legitimate registration warn. The code-keyed parser registrars (on `ProtocolBase`, `Link`, `Internet`, `Frame`, `PCAPNG`, `Transport` and `SCTP`) now name the displaced entry and its replacement, where they reported only that something was overwritten. Their firing condition is harmonised onto the narrower guard [#681](https://github.com/JarryShaw/PyPCAPKit/pull/681) gave `register_protocol` ([#718](https://github.com/JarryShaw/PyPCAPKit/issues/718), [#726](https://github.com/JarryShaw/PyPCAPKit/pull/726)) rather than presence alone: that key is derived from the value it stores, whereas these take a caller-supplied `code`. Where a table is pre-seeded with an unresolved `ModuleDescriptor`, the guard's `is` comparison measures the resolved incoming class against the descriptor, so re-registering the same class under its own pre-seeded code (`Internet.register(TransType.TCP, TCP)`) still warns, a false positive the harmonised guard does not close. `ContextRegistry.register()` already raises on a duplicate, and the reassembly and ESP registrars append to lists with no key, so none of them takes a guard. `import pcapkit` emits no `RegistryWarning`: none of its registry writes (`R1CounterParameter`'s second code, from [#690](https://github.com/JarryShaw/PyPCAPKit/issues/690), among them) lands on a key already present ([#692](https://github.com/JarryShaw/PyPCAPKit/issues/692)). -- every EOF-truncated PCAP-NG file raised a bare `ValueError` out of `Extractor`, losing the whole extraction rather than the one truncated block. Block Total Length is cross-checked against its trailing copy and never against the file, so a last block declaring more than the file holds left `PCAPNG.read()` seeking *past* the end, and the next block read measured a **negative** remainder that `io.RawIOBase.read` refused. The seek is now clamped to the octets `_read_fileng` returned, with a `ProtocolWarning` naming the overrun, and a tail under the twelve octets a block needs is the quiet `StreamEOFError` that `Extractor.record_frames()` already catches (deliberately not a clamp, which would fabricate a frame out of zero padding). Another route to the same shape is closed by a new `nonnegative()` helper, composed into `bounded_option` and `bounded_area` and applied to the unwrapped `length - N` spans and the `__option_padding__` fields: `_TextField.__call__()` builds `f'{length}s'` unconditionally, so a negative became `'-8s'` and `struct.calcsize` raised an exception caught nowhere a frame loop looks. Both of [#678](https://github.com/JarryShaw/PyPCAPKit/issues/678)'s failure families are gone; others remain: `ValueError: N is not a valid BlockType` (filed as [#701](https://github.com/JarryShaw/PyPCAPKit/issues/701)) and `MemoryError` from [#593](https://github.com/JarryShaw/PyPCAPKit/pull/593)'s 32-bit band. Bare exceptions in `SystemdJournalExportBlock.post_process()`, reachable from valid input, are fixed with it: the body's NUL alignment padding was read as a field *name* (`struct.error`; a NUL-only line now ends the entry); a binary field's declared length was unbounded against its entry (`OverflowError`; now a `ProtocolError` that keeps the block's journal data as captured, with a `ProtocolWarning`, [#1406](https://github.com/JarryShaw/PyPCAPKit/issues/1406)); and a non-UTF-8 name, key or value raised `UnicodeDecodeError` (now `errors='replace'` and reported). The silent loss of every field after a binary one is filed as [#704](https://github.com/JarryShaw/PyPCAPKit/issues/704). The unclamped non-packet option areas keep the per-block framing assumption [#676](https://github.com/JarryShaw/PyPCAPKit/pull/676)'s note describes. Also here, the last registrar with no overwrite guard: `Option.register()` now reports a displaced option schema as a `RegistryWarning` naming every namespace the `ns='opt'` fan-out displaced something in, tested by membership (subscripting the per-namespace `defaultdict` would insert `UnknownOption` for a code nobody registered). The condition is identity-based like the parser registrars and [#681](https://github.com/JarryShaw/PyPCAPKit/pull/681)'s guard for `register_protocol` ([#718](https://github.com/JarryShaw/PyPCAPKit/issues/718), [#726](https://github.com/JarryShaw/PyPCAPKit/pull/726)); a namespace the call itself creates is exempt. Flooring the decryption-secrets payloads that read `__length__` *whole* was tried and reverted, since `Schema.pack()` leaves that key at `-1` for "unknown". Ships labelled breaking: well-formed captures are byte-identical, but any truncated PCAP-NG now yields the frames before the cut where it raised, the last of which may carry zero-padded octets, and some short-file depths move from `ProtocolError: unknown byteorder magic` to `StreamEOFError`. The [#683](https://github.com/JarryShaw/PyPCAPKit/pull/683) drift [#685](https://github.com/JarryShaw/PyPCAPKit/issues/685) tracks is unchanged ([#678](https://github.com/JarryShaw/PyPCAPKit/issues/678)). +- every EOF-truncated PCAP-NG file raised a bare `ValueError` out of `Extractor`, losing the whole extraction rather than the one truncated block. Block Total Length is cross-checked against its trailing copy and never against the file, so a last block declaring more than the file holds left `PCAPNG.read()` seeking *past* the end, and the next block read measured a **negative** remainder that `io.RawIOBase.read` refused. The seek is now clamped to the octets `_read_fileng` returned, with a `ProtocolWarning` naming the overrun, and a tail under the twelve octets a block needs is the quiet `StreamEOFError` that `Extractor.record_frames()` already catches (deliberately not a clamp, which would fabricate a frame out of zero padding). Another route to the same shape is closed by a new `nonnegative()` helper, composed into `bounded_option` and `bounded_area` and applied to the unwrapped `length - N` spans and the `__option_padding__` fields: `_TextField.__call__()` builds `f'{length}s'` unconditionally, so a negative became `'-8s'` and `struct.calcsize` raised an exception caught nowhere a frame loop looks. Both of [#678](https://github.com/JarryShaw/PyPCAPKit/issues/678)'s failure families are gone; others remain: `ValueError: N is not a valid BlockType` (filed as [#701](https://github.com/JarryShaw/PyPCAPKit/issues/701)) and `MemoryError` from [#593](https://github.com/JarryShaw/PyPCAPKit/pull/593)'s 32-bit band. Bare exceptions in `SystemdJournalExportBlock.post_process()`, reachable from valid input, are fixed with it: the body's NUL alignment padding was read as a field *name* (`struct.error`; a NUL-only line now ends the entry); a binary field's declared length was unbounded against its entry (`OverflowError`; now a `ProtocolError` that keeps the block's journal data as captured, with a `ProtocolWarning`, [#1406](https://github.com/JarryShaw/PyPCAPKit/issues/1406)); and a non-UTF-8 name, key or value raised `UnicodeDecodeError` (now `errors='replace'` and reported). The silent loss of every field after a binary one is filed as [#704](https://github.com/JarryShaw/PyPCAPKit/issues/704). The unclamped non-packet option areas keep the per-block framing assumption [#676](https://github.com/JarryShaw/PyPCAPKit/pull/676)'s note describes. Also here, the last registrar with no overwrite guard: `Option.register()` now reports a displaced option schema as a `RegistryWarning` naming every namespace the `ns='opt'` fan-out displaced something in, tested by membership (subscripting the per-namespace `defaultdict` would insert `UnknownOption` for a code nobody registered). The condition is identity-based like the parser registrars and [#681](https://github.com/JarryShaw/PyPCAPKit/pull/681)'s guard for `register_protocol` ([#718](https://github.com/JarryShaw/PyPCAPKit/issues/718), [#726](https://github.com/JarryShaw/PyPCAPKit/pull/726)); a namespace the call itself creates is exempt. Flooring the decryption-secrets payloads that read `__length__` *whole* was tried and reverted, since `Schema.pack()` leaves that key at `-1` for "unknown". Ships labelled breaking: well-formed captures are byte-identical, but any truncated PCAP-NG now yields the frames before the cut where it raised, the last of which may carry zero-padded octets, and some short-file depths move from `ProtocolError: PCAP-NG: unknown byte-order magic` to `StreamEOFError`. The [#683](https://github.com/JarryShaw/PyPCAPKit/pull/683) drift [#685](https://github.com/JarryShaw/PyPCAPKit/issues/685) tracks is unchanged ([#678](https://github.com/JarryShaw/PyPCAPKit/issues/678)). - HIP's `R1_COUNTER`/`R1_Counter` parameter packed 12 octets where [RFC 7401 Section 5.2.3](https://datatracker.ietf.org/doc/html/rfc7401#section-5.2.3) requires 16, and `LOCATOR_SET` declared its `Length` in 4-octet units where [RFC 7401 Section 5.2.1](https://datatracker.ietf.org/doc/html/rfc7401#section-5.2.1)'s `Length` is a byte count -- independent defects, fixed together because the evidence for either needed the other out of the way. `R1CounterParameter`'s `counter` was a `UInt32Field` where the RFC states the width twice, "8 bytes" in the diagram and "a 64-bit unsigned integer" in prose; both versions whose *packing* reaches the class, `R1_Counter` (128) and `R1_COUNTER` (129), packed a 12-octet record at `4 (mod 8)`. It is a `UInt64Field` now and the record is 16 octets ([#672](https://github.com/JarryShaw/PyPCAPKit/issues/672)). `LocatorSetParameter`'s `Length` was `sum(Locator.len)`, in the 4-octet units [RFC 8046 Section 4](https://datatracker.ietf.org/doc/html/rfc8046#section-4) gives `Locator Length`; a set of *n* plain IPv6 locators declared `4n` octets against `24n` present, and `Schema.unpack()` handed the nested `ListField` only the declared octets, so `n = 2` and `n = 5` parsed one truncated locator and left the rest unconsumed. `HIP._make_param_locator_set()` now sums `8 + Locator.len * 4` per locator ([#679](https://github.com/JarryShaw/PyPCAPKit/issues/679)). Another defect in the same parameter's padding, which [#651](https://github.com/JarryShaw/PyPCAPKit/issues/651) left alone because it cancelled this one, is fixed alongside: the nested `Locator` schemas share the parameter's packet context and pack their own `len` over it, so `padding`, evaluated after the list, saw 4 rather than the real length. A new `locator_set_len_callback` snapshots `Length` under a private key before any locator packs, and `locator_set_padding_len` reads that snapshot. The `counter` defaulted to `0` and the width defect could not be told apart from it, so the fixture now overrides both `R1_COUNTER` codes with `counter=0xaabbccdd`; new tests are `test_hip_r1_counter_width_unit.py` and `test_hip_locator_set_length_unit.py`. The `hip-parameter/R1_Counter` `EXPECTED_FAILURES` entry (code 128 registered no schema of its own, since `R1CounterParameter` declared only `code=129`) was deleted once that registry defect was fixed as [#690](https://github.com/JarryShaw/PyPCAPKit/issues/690). `HIP_COPIES` dropped to one as [#689](https://github.com/JarryShaw/PyPCAPKit/issues/689), once [#672](https://github.com/JarryShaw/PyPCAPKit/issues/672) and [#679](https://github.com/JarryShaw/PyPCAPKit/issues/679) left it routing around nothing. - `httpv1.HTTP` raised a bare exception for any payload that is not an HTTP/1 message, at these sites: the header/body split in `read`, the header's CRLF split and the start-line split in `_read_http_header`, and `item[1]` on a field line with no colon. `HTTP._guess_version()` falls through on `ProtocolError` alone, so the HTTP/1 attempt aborted the guess and the HTTP/2 arm below was dead code for each of those inputs. Each site now raises `ProtocolError`, which each method's `Raises:` section documented, and a test pins the invariant (nothing escapes `httpv1.HTTP` that is not a `ProtocolError`), so any further bare-raising site would fail there. An obs-fold continuation line [[RFC 9112 Section 5.2](https://datatracker.ietf.org/doc/html/rfc9112#section-5.2)] is legal HTTP/1 and is now unfolded, the RFC's remedy, rather than read as a field of its own (with no colon it raised that `IndexError`; with one it parsed into a spurious extra field). Reaching the HTTP/2 arm exposed a payload under nine octets to `httpv2.HTTP`, where it usually fails inside the schema with a bare `struct.error` (not a `ValueError`, `ProtocolError` or pcapkit `StructError`, so uncatchable); `read` converts it and `_guess_version` suppresses it on the last arm only, since an arm that is not last hands the payload onward when it swallows. Separately, `PayloadField.protocol` looked a `str` up in `__proto__` as given while the registry is keyed on the upper-cased class name, so a name not already upper-case resolved to `None` and yielded `Raw`; it now folds case and warns with `RegistryWarning` for a genuinely unregistered name, and `__init__` assigns through that property instead of writing `_protocol` past it ([#787](https://github.com/JarryShaw/PyPCAPKit/issues/787), [#789](https://github.com/JarryShaw/PyPCAPKit/pull/789)). - a systemd journal entry whose last field lacks the format's mandatory trailing newline let the PCAP-NG block's 32-bit alignment padding be read as part of that field's value, with no exception or warning: `b'MESSAGE=hello'` plus three NULs returned `MESSAGE == 'hello\x00\x00\x00'`. The padding-only-line guard from [#678](https://github.com/JarryShaw/PyPCAPKit/issues/678) catches padding only on a line of its own, which needs the preceding field to have ended with a real newline; without one `readline()` runs through the value into the padding. Reachable on text fields only (a binary field's length-prefixed read is never in play, and a binary field *name* on such a line fails its own length-prefix read regardless). A field line with no terminating newline raises `ProtocolError`, so the block's journal data is kept as captured, padding included, with a `ProtocolWarning` ([#794](https://github.com/JarryShaw/PyPCAPKit/issues/794), [#1406](https://github.com/JarryShaw/PyPCAPKit/issues/1406)). `examples/generators/pcapng.py`'s `_journal_entry` cited a bare line number for the defect its alignment workaround avoids and mis-attributed the fix; it now names [#678](https://github.com/JarryShaw/PyPCAPKit/issues/678)'s `not line.strip(b'\x00')` guard and the mechanism ([#791](https://github.com/JarryShaw/PyPCAPKit/issues/791)). diff --git a/docs/source/changelog/1.5.0.rst b/docs/source/changelog/1.5.0.rst index fe84b483bb..b4ef29685b 100644 --- a/docs/source/changelog/1.5.0.rst +++ b/docs/source/changelog/1.5.0.rst @@ -2139,8 +2139,9 @@ Fixed key at ``-1`` for "unknown". Ships labelled breaking: well-formed captures are byte-identical, but any truncated PCAP-NG now yields the frames before the cut where it raised, the last of which may carry zero-padded octets, and some short-file depths - move from ``ProtocolError: unknown byteorder magic`` to :exc:`.StreamEOFError`. The - :pr:`683` drift :issue:`685` tracks is unchanged (:issue:`678`). + move from ``ProtocolError: PCAP-NG: unknown byte-order magic`` to + :exc:`.StreamEOFError`. The :pr:`683` drift :issue:`685` tracks is unchanged + (:issue:`678`). * HIP's ``R1_COUNTER``/``R1_Counter`` parameter packed 12 octets where :rfc:`7401#section-5.2.3` requires 16, and ``LOCATOR_SET`` declared its ``Length`` in 4-octet units where :rfc:`7401#section-5.2.1`'s ``Length`` is a byte count -- diff --git a/pcapkit/protocols/misc/pcap/header.py b/pcapkit/protocols/misc/pcap/header.py index 59901f2a80..71da285923 100644 --- a/pcapkit/protocols/misc/pcap/header.py +++ b/pcapkit/protocols/misc/pcap/header.py @@ -367,14 +367,14 @@ def _make_magic(self, byteorder: 'Literal["big", "little"]' = sys.byteorder, """ if lilendian is not None and bigendian is not None and lilendian == bigendian: - raise EndianError('unresolved byte order') + raise EndianError('PCAP: unresolved byte order') if bigendian is not None: byteorder = 'big' if bigendian else 'little' elif lilendian is not None: byteorder = 'little' if lilendian else 'big' if byteorder not in ('little', 'big'): - raise EndianError(f"unknown byte order: {byteorder!r}") + raise EndianError(f'PCAP: unknown byte order: {byteorder!r}') magic_number = _DECLARED_MAGIC[(byteorder, nanosecond)] return magic_number, (byteorder == 'little') diff --git a/pcapkit/protocols/misc/pcapng.py b/pcapkit/protocols/misc/pcapng.py index 2abd0b3210..09e664cdf8 100644 --- a/pcapkit/protocols/misc/pcapng.py +++ b/pcapkit/protocols/misc/pcapng.py @@ -335,6 +335,29 @@ def _unassigned_member(enum_cls: 'Type[_FlagsEnum]', value: 'int') -> '_FlagsEnu return obj +def _block_type_name(block_type: 'Enum_BlockType') -> 'str': + """Return the name of a block type, for an option's placement message. + + Args: + block_type: The block type to name. + + Returns: + The block type's name, e.g. ``Enhanced_Packet_Block``. + + Note: + ``block_type`` is looked up rather than having its :attr:`~enum.Enum.name` + read directly, because :attr:`PCAPNG._type ` is only an + :class:`~pcapkit.const.pcapng.block_type.BlockType` on the parse path. + On the make path :meth:`~pcapkit.protocols.protocol.Protocol._make_index` + returns a plain :obj:`int`, which the assignment in + :meth:`PCAPNG.make ` only + :func:`~typing.cast` to the enumeration, so reading ``.name`` off it + there raises :exc:`AttributeError` (:issue:`2301`). + + """ + return Enum_BlockType(block_type).name + + class PacketDirection(EnumLookup, enum.IntEnum): """Packet direction for ``epb_flags`` options. @@ -3102,7 +3125,7 @@ def _read_option_endofopt(self, schema: 'Schema_EndOfOption', *, """ if self._opt[schema.type] > 0: raise ProtocolError(f'PCAP-NG: [opt_endofopt] option must be only one, ' - f'but {self._opt[schema.type] + 1} found.') + 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})') @@ -3187,10 +3210,10 @@ 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, ' - f'but found in {self._type} 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'but {self._opt[schema.type] + 1} found.') + f'not {self._opt[schema.type] + 1}') option = Data_IF_NameOption( type=schema.type, @@ -3213,10 +3236,10 @@ 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, ' - f'but found in {self._type} 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'but {self._opt[schema.type] + 1} found.') + f'not {self._opt[schema.type] + 1}') option = Data_IF_DescriptionOption( type=schema.type, @@ -3239,7 +3262,7 @@ 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, ' - f'but found in {self._type} 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})') @@ -3264,7 +3287,7 @@ def _read_option_if_ipv6(self, schema: 'Schema_IF_IPv6AddrOption', *, """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_IPv6addr] option must be in Interface Description Block, ' - f'but found in {self._type} 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})') @@ -3289,10 +3312,10 @@ def _read_option_if_mac(self, schema: 'Schema_IF_MACAddrOption', *, """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_MACaddr] option must be in Interface Description Block, ' - f'but found in {self._type} 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'but {self._opt[schema.type] + 1} found.') + 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})') @@ -3317,10 +3340,10 @@ def _read_option_if_eui(self, schema: 'Schema_IF_EUIAddrOption', *, """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_EUIaddr] option must be in Interface Description Block, ' - f'but found in {self._type} 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'but {self._opt[schema.type] + 1} found.') + 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})') @@ -3345,10 +3368,10 @@ def _read_option_if_speed(self, schema: 'Schema_IF_SpeedOption', *, """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_speed] option must be in Interface Description Block, ' - f'but found in {self._type} 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'but {self._opt[schema.type] + 1} found.') + 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})') @@ -3373,10 +3396,10 @@ def _read_option_if_tsresol(self, schema: 'Schema_IF_TSResolOption', *, """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_tsresol] option must be in Interface Description Block, ' - f'but found in {self._type} 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'but {self._opt[schema.type] + 1} found.') + 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})') @@ -3401,10 +3424,10 @@ def _read_option_if_tzone(self, schema: 'Schema_IF_TZoneOption', *, """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_tzone] option must be in Interface Description Block, ' - f'but found in {self._type} 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'but {self._opt[schema.type] + 1} found.') + 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})') @@ -3439,10 +3462,10 @@ def _read_option_if_filter(self, schema: 'Schema_IF_FilterOption', *, """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_filter] option must be in Interface Description Block, ' - f'but found in {self._type} 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'but {self._opt[schema.type] + 1} found.') + 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})') @@ -3468,10 +3491,10 @@ def _read_option_if_os(self, schema: 'Schema_IF_OSOption', *, """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_os] option must be in Interface Description Block, ' - f'but found in {self._type} 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, ' - f'but {self._opt[schema.type] + 1} found.') + f'not {self._opt[schema.type] + 1}') option = Data_IF_OSOption( type=schema.type, @@ -3494,10 +3517,10 @@ def _read_option_if_fcslen(self, schema: 'Schema_IF_FCSLenOption', *, """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_fcslen] option must be in Interface Description Block, ' - f'but found in {self._type} 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'but {self._opt[schema.type] + 1} found.') + 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})') @@ -3522,10 +3545,10 @@ def _read_option_if_tsoffset(self, schema: 'Schema_IF_TSOffsetOption', *, """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_tsoffset] option must be in Interface Description Block, ' - f'but found in {self._type} 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'but {self._opt[schema.type] + 1} found.') + 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})') @@ -3550,10 +3573,10 @@ def _read_option_if_hardware(self, schema: 'Schema_IF_HardwareOption', *, """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_hardware] option must be in Interface Description Block, ' - f'but found in {self._type} 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, ' - f'but {self._opt[schema.type] + 1} found.') + f'not {self._opt[schema.type] + 1}') option = Data_IF_HardwareOption( type=schema.type, @@ -3576,10 +3599,10 @@ def _read_option_if_txspeed(self, schema: 'Schema_IF_TxSpeedOption', *, """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_txspeed] option must be in Interface Description Block, ' - f'but found in {self._type} 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, ' - f'but {self._opt[schema.type] + 1} found.') + 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})') @@ -3604,10 +3627,10 @@ def _read_option_if_rxspeed(self, schema: 'Schema_IF_RxSpeedOption', *, """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_rxspeed] option must be in Interface Description Block, ' - f'but found in {self._type} 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'but {self._opt[schema.type] + 1} found.') + 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})') @@ -3632,10 +3655,10 @@ def _read_option_epb_flags(self, schema: 'Schema_EPB_FlagsOption', *, """ if self._type != Enum_BlockType.Enhanced_Packet_Block: raise ProtocolError(f'PCAP-NG: [epb_flags] option must be in Enhanced Packet Block, ' - f'but found in {self._type} 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, ' - f'but {self._opt[schema.type] + 1} found.') + 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})') @@ -3674,7 +3697,7 @@ def _read_option_epb_hash(self, schema: 'Schema_EPB_HashOption', *, """ if self._type != Enum_BlockType.Enhanced_Packet_Block: raise ProtocolError(f'PCAP-NG: [epb_hash] option must be in Enhanced Packet Block, ' - f'but found in {self._type} block.') + f'not {_block_type_name(self._type)}') option = Data_EPB_HashOption( type=schema.type, @@ -3698,10 +3721,10 @@ def _read_option_epb_dropcount(self, schema: 'Schema_EPB_DropCountOption', *, """ if self._type != Enum_BlockType.Enhanced_Packet_Block: raise ProtocolError(f'PCAP-NG: [epb_dropcount] option must be in Enhanced Packet Block, ' - f'but found in {self._type} 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, ' - f'but {self._opt[schema.type] + 1} found.') + 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})') @@ -3726,10 +3749,10 @@ def _read_option_epb_packetid(self, schema: 'Schema_EPB_PacketIDOption', *, """ if self._type != Enum_BlockType.Enhanced_Packet_Block: raise ProtocolError(f'PCAP-NG: [epb_packetid] option must be in Enhanced Packet Block, ' - f'but found in {self._type} 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, ' - f'but {self._opt[schema.type] + 1} found.') + 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})') @@ -3754,10 +3777,10 @@ def _read_option_epb_queue(self, schema: 'Schema_EPB_QueueOption', *, """ if self._type != Enum_BlockType.Enhanced_Packet_Block: raise ProtocolError(f'PCAP-NG: [epb_queue] option must be in Enhanced Packet Block, ' - f'but found in {self._type} 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'but {self._opt[schema.type] + 1} found.') + 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})') @@ -3782,7 +3805,7 @@ def _read_option_epb_verdict(self, schema: 'Schema_EPB_VerdictOption', *, """ if self._type != Enum_BlockType.Enhanced_Packet_Block: raise ProtocolError(f'PCAP-NG: [epb_verdict] option must be in Enhanced Packet Block, ' - f'but found in {self._type} 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})') @@ -3808,10 +3831,10 @@ def _read_option_ns_dnsname(self, schema: 'Schema_NS_DNSNameOption', *, """ if self._type != Enum_BlockType.Name_Resolution_Block: raise ProtocolError(f'PCAP-NG: [ns_dnsname] option must be in Name Resolution Block, ' - f'but found in {self._type} 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'but {self._opt[schema.type] + 1} found.') + f'not {self._opt[schema.type] + 1}') option = Data_NS_DNSNameOption( type=schema.type, @@ -3834,10 +3857,10 @@ def _read_option_ns_dnsipv4(self, schema: 'Schema_NS_DNSIP4AddrOption', *, """ if self._type != Enum_BlockType.Name_Resolution_Block: raise ProtocolError(f'PCAP-NG: [ns_dnsIP4addr] option must be in Name Resolution Block, ' - f'but found in {self._type} 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'but {self._opt[schema.type] + 1} found.') + 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})') @@ -3862,10 +3885,10 @@ def _read_option_ns_dnsipv6(self, schema: 'Schema_NS_DNSIP6AddrOption', *, """ if self._type != Enum_BlockType.Name_Resolution_Block: raise ProtocolError(f'PCAP-NG: [ns_dnsIP6addr] option must be in Name Resolution Block, ' - f'but found in {self._type} 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'but {self._opt[schema.type] + 1} found.') + 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})') @@ -3890,10 +3913,10 @@ def _read_option_isb_starttime(self, schema: 'Schema_ISB_StartTimeOption', *, """ if self._type != Enum_BlockType.Interface_Statistics_Block: raise ProtocolError(f'PCAP-NG: [isb_starttime] option must be in Interface Statistics Block, ' - f'but found in {self._type} 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'but {self._opt[schema.type] + 1} found.') + 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})') @@ -3922,10 +3945,10 @@ def _read_option_isb_endtime(self, schema: 'Schema_ISB_EndTimeOption', *, """ if self._type != Enum_BlockType.Interface_Statistics_Block: raise ProtocolError(f'PCAP-NG: [isb_endtime] option must be in Interface Statistics Block, ' - f'but found in {self._type} 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'but {self._opt[schema.type] + 1} found.') + 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})') @@ -3954,10 +3977,10 @@ def _read_option_isb_ifrecv(self, schema: 'Schema_ISB_IFRecvOption', *, """ if self._type != Enum_BlockType.Interface_Statistics_Block: raise ProtocolError(f'PCAP-NG: [isb_ifrecv] option must be in Interface Statistics Block, ' - f'but found in {self._type} 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'but {self._opt[schema.type] + 1} found.') + 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})') @@ -3982,10 +4005,10 @@ def _read_option_isb_ifdrop(self, schema: 'Schema_ISB_IFDropOption', *, """ if self._type != Enum_BlockType.Interface_Statistics_Block: raise ProtocolError(f'PCAP-NG: [isb_ifdrop] option must be in Interface Statistics Block, ' - f'but found in {self._type} 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'but {self._opt[schema.type] + 1} found.') + 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})') @@ -4010,10 +4033,10 @@ def _read_option_isb_filteraccept(self, schema: 'Schema_ISB_FilterAcceptOption', """ if self._type != Enum_BlockType.Interface_Statistics_Block: raise ProtocolError(f'PCAP-NG: [isb_filteraccept] option must be in Interface Statistics Block, ' - f'but found in {self._type} 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'but {self._opt[schema.type] + 1} found.') + 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})') @@ -4038,10 +4061,10 @@ def _read_option_isb_osdrop(self, schema: 'Schema_ISB_OSDropOption', *, """ if self._type != Enum_BlockType.Interface_Statistics_Block: raise ProtocolError(f'PCAP-NG: [isb_osdrop] option must be in Interface Statistics Block, ' - f'but found in {self._type} 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'but {self._opt[schema.type] + 1} found.') + 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})') @@ -4066,10 +4089,10 @@ def _read_option_isb_usrdeliv(self, schema: 'Schema_ISB_UsrDelivOption', *, """ if self._type != Enum_BlockType.Interface_Statistics_Block: raise ProtocolError(f'PCAP-NG: [isb_usrdeliv] option must be in Interface Statistics Block, ' - f'but found in {self._type} 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'but {self._opt[schema.type] + 1} found.') + 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})') @@ -4094,10 +4117,10 @@ def _read_option_pack_flags(self, schema: 'Schema_PACK_FlagsOption', *, """ if self._type != Enum_BlockType.Packet_Block: raise ProtocolError(f'PCAP-NG: [pack_flags] option must be in Packet Block, ' - f'but found in {self._type} 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'but {self._opt[schema.type] + 1} found.') + 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})') @@ -4136,7 +4159,7 @@ def _read_option_pack_hash(self, schema: 'Schema_PACK_HashOption', *, """ if self._type != Enum_BlockType.Packet_Block: raise ProtocolError(f'PCAP-NG: [pack_hash] option must be in Packet Block, ' - f'but found in {self._type} block.') + f'not {_block_type_name(self._type)}') option = Data_PACK_HashOption( type=schema.type, @@ -5192,7 +5215,7 @@ def _make_option_endofopt(self, type: 'Enum_OptionType', option: 'Optional[Data_ """ if self._opt[type] > 0: raise ProtocolError(f'PCAP-NG: [opt_endofopt] option must be only one, ' - f'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') return Schema_EndOfOption( type=type, @@ -5268,10 +5291,10 @@ def _make_option_if_name(self, type: 'Enum_OptionType', option: 'Optional[Data_I """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_name] option must be in Interface Description Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: name = option.name @@ -5299,10 +5322,10 @@ def _make_option_if_description(self, type: 'Enum_OptionType', option: 'Optional """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_description] option must be in Interface Description Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: description = option.description @@ -5330,7 +5353,7 @@ def _make_option_if_ipv4(self, type: 'Enum_OptionType', option: 'Optional[Data_I """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_IPv4addr] option must be in Interface Description Block, ' - f'but found in {self._type} block.') + f'not {_block_type_name(self._type)}') if option is not None: interface = option.interface @@ -5358,7 +5381,7 @@ def _make_option_if_ipv6(self, type: 'Enum_OptionType', option: 'Optional[Data_I """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_IPv6addr] option must be in Interface Description Block, ' - f'but found in {self._type} block.') + f'not {_block_type_name(self._type)}') if option is not None: interface = option.interface @@ -5386,10 +5409,10 @@ def _make_option_if_mac(self, type: 'Enum_OptionType', option: 'Optional[Data_IF """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_MACaddr] option must be in Interface Description Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: interface = option.interface @@ -5417,10 +5440,10 @@ def _make_option_if_eui(self, type: 'Enum_OptionType', option: 'Optional[Data_IF """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_EUIaddr] option must be in Interface Description Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: interface = option.interface @@ -5448,10 +5471,10 @@ def _make_option_if_speed(self, type: 'Enum_OptionType', option: 'Optional[Data_ """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_speed] option must be in Interface Description Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: speed = option.speed @@ -5479,10 +5502,10 @@ def _make_option_if_tsresol(self, type: 'Enum_OptionType', option: 'Optional[Dat """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_tsresol] option must be in Interface Description Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: resolution = option.resolution @@ -5498,7 +5521,7 @@ def _make_option_if_tsresol(self, type: 'Enum_OptionType', option: 'Optional[Dat resl = int(math.log2(resolution)) else: raise ProtocolError(f'PCAP-NG: [if_tsresol] option resolution must be power of 10 or 2, ' - f'but {resolution} found.') + f'not {resolution}') return Schema_IF_TSResolOption( type=type, @@ -5527,10 +5550,10 @@ def _make_option_if_tzone(self, type: 'Enum_OptionType', option: 'Optional[Data_ """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_tzone] option must be in Interface Description Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: tzone = option.timezone @@ -5543,7 +5566,7 @@ def _make_option_if_tzone(self, type: 'Enum_OptionType', option: 'Optional[Data_ 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'but {builtins.type(tzone).__name__} found.') + f'not {builtins.type(tzone).__name__}') return Schema_IF_TZoneOption( type=type, @@ -5576,10 +5599,10 @@ def _make_option_if_filter(self, type: 'Enum_OptionType', option: 'Optional[Data """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_filter] option must be in Interface Description Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: filter_val = option.code @@ -5613,10 +5636,10 @@ def _make_option_if_os(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_ """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_os] option must be in Interface Description Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: os = option.os @@ -5644,10 +5667,10 @@ def _make_option_if_fcslen(self, type: 'Enum_OptionType', option: 'Optional[Data """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_fcslen] option must be in Interface Description Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: fcs_length = option.fcs_length @@ -5675,10 +5698,10 @@ def _make_option_if_tsoffset(self, type: 'Enum_OptionType', option: 'Optional[Da """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_tsoffset] option must be in Interface Description Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: offset = option.offset @@ -5706,10 +5729,10 @@ def _make_option_if_hardware(self, type: 'Enum_OptionType', option: 'Optional[Da """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_hardware] option must be in Interface Description Block, ' - f'but found in {self._type} 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, ' - f'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: hardware = option.hardware @@ -5737,10 +5760,10 @@ def _make_option_if_txspeed(self, type: 'Enum_OptionType', option: 'Optional[Dat """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_txspeed] option must be in Interface Description Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: speed = option.speed @@ -5768,10 +5791,10 @@ def _make_option_if_rxspeed(self, type: 'Enum_OptionType', option: 'Optional[Dat """ if self._type != Enum_BlockType.Interface_Description_Block: raise ProtocolError(f'PCAP-NG: [if_rxspeed] option must be in Interface Description Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: speed = option.speed @@ -5839,10 +5862,10 @@ def _make_option_epb_flags(self, type: 'Enum_OptionType', option: 'Optional[Data """ if self._type != Enum_BlockType.Enhanced_Packet_Block: raise ProtocolError(f'PCAP-NG: [epb_flags] option must be in Enhanced Packet Block, ' - f'but found in {self._type} 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, ' - f'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: direction_val = option.direction @@ -5913,7 +5936,7 @@ def _make_option_epb_hash(self, type: 'Enum_OptionType', option: 'Optional[Data_ """ if self._type != Enum_BlockType.Enhanced_Packet_Block: raise ProtocolError(f'PCAP-NG: [epb_hash] option must be in Enhanced Packet Block, ' - f'but found in {self._type} block.') + f'not {_block_type_name(self._type)}') if option is not None: algo_val = option.algorithm @@ -5946,10 +5969,10 @@ def _make_option_epb_dropcount(self, type: 'Enum_OptionType', option: 'Optional[ """ if self._type != Enum_BlockType.Enhanced_Packet_Block: raise ProtocolError(f'PCAP-NG: [epb_dropcount] option must be in Enhanced Packet Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: drop_count = option.drop_count @@ -5977,10 +6000,10 @@ def _make_option_epb_packetid(self, type: 'Enum_OptionType', option: 'Optional[D """ if self._type != Enum_BlockType.Enhanced_Packet_Block: raise ProtocolError(f'PCAP-NG: [epb_packetid] option must be in Enhanced Packet Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: packet_id = option.packet_id @@ -6008,10 +6031,10 @@ def _make_option_epb_queue(self, type: 'Enum_OptionType', option: 'Optional[Data """ if self._type != Enum_BlockType.Enhanced_Packet_Block: raise ProtocolError(f'PCAP-NG: [epb_queue] option must be in Enhanced Packet Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: queue_id = option.queue_id @@ -6047,7 +6070,7 @@ def _make_option_epb_verdict(self, type: 'Enum_OptionType', option: 'Optional[Da """ if self._type != Enum_BlockType.Enhanced_Packet_Block: raise ProtocolError(f'PCAP-NG: [epb_verdict] option must be in Enhanced Packet Block, ' - f'but found in {self._type} 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. @@ -6082,10 +6105,10 @@ def _make_option_ns_dnsname(self, type: 'Enum_OptionType', option: 'Optional[Dat """ if self._type != Enum_BlockType.Name_Resolution_Block: raise ProtocolError(f'PCAP-NG: [ns_dnsname] option must be in Name Resolution Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: name = option.name @@ -6113,10 +6136,10 @@ def _make_option_ns_dnsipv4(self, type: 'Enum_OptionType', option: 'Optional[Dat """ if self._type != Enum_BlockType.Name_Resolution_Block: raise ProtocolError(f'PCAP-NG: [ns_dnsIP4addr] option must be in Name Resolution Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: ip = option.ip @@ -6144,10 +6167,10 @@ def _make_option_ns_dnsipv6(self, type: 'Enum_OptionType', option: 'Optional[Dat """ if self._type != Enum_BlockType.Name_Resolution_Block: raise ProtocolError(f'PCAP-NG: [ns_dnsIP6addr] option must be in Name Resolution Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: ip = option.ip @@ -6175,10 +6198,10 @@ def _make_option_isb_starttime(self, type: 'Enum_OptionType', option: 'Optional[ """ if self._type != Enum_BlockType.Interface_Statistics_Block: raise ProtocolError(f'PCAP-NG: [isb_starttime] option must be in Interface Statistics Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: timestamp = option.timestamp_epoch @@ -6208,10 +6231,10 @@ def _make_option_isb_endtime(self, type: 'Enum_OptionType', option: 'Optional[Da """ if self._type != Enum_BlockType.Interface_Statistics_Block: raise ProtocolError(f'PCAP-NG: [isb_endtime] option must be in Interface Statistics Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: timestamp = option.timestamp_epoch @@ -6241,10 +6264,10 @@ def _make_option_isb_ifrecv(self, type: 'Enum_OptionType', option: 'Optional[Dat """ if self._type != Enum_BlockType.Interface_Statistics_Block: raise ProtocolError(f'PCAP-NG: [isb_ifrecv] option must be in Interface Statistics Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: packets = option.packets @@ -6272,10 +6295,10 @@ def _make_option_isb_ifdrop(self, type: 'Enum_OptionType', option: 'Optional[Dat """ if self._type != Enum_BlockType.Interface_Statistics_Block: raise ProtocolError(f'PCAP-NG: [isb_ifdrop] option must be in Interface Statistics Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: packets = option.packets @@ -6303,10 +6326,10 @@ def _make_option_isb_filteraccept(self, type: 'Enum_OptionType', option: 'Option """ if self._type != Enum_BlockType.Interface_Statistics_Block: raise ProtocolError(f'PCAP-NG: [isb_filteraccept] option must be in Interface Statistics Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: packets = option.packets @@ -6334,10 +6357,10 @@ def _make_option_isb_osdrop(self, type: 'Enum_OptionType', option: 'Optional[Dat """ if self._type != Enum_BlockType.Interface_Statistics_Block: raise ProtocolError(f'PCAP-NG: [isb_osdrop] option must be in Interface Statistics Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: packets = option.packets @@ -6365,10 +6388,10 @@ def _make_option_isb_usrdeliv(self, type: 'Enum_OptionType', option: 'Optional[D """ if self._type != Enum_BlockType.Interface_Statistics_Block: raise ProtocolError(f'PCAP-NG: [isb_usrdeliv] option must be in Interface Statistics Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: packets = option.packets @@ -6436,10 +6459,10 @@ def _make_option_pack_flags(self, type: 'Enum_OptionType', option: 'Optional[Dat """ if self._type != Enum_BlockType.Packet_Block: raise ProtocolError(f'PCAP-NG: [pack_flags] option must be in Packet Block, ' - f'but found in {self._type} 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'but {self._opt[type] + 1} found.') + f'not {self._opt[type] + 1}') if option is not None: direction_val = option.direction @@ -6510,7 +6533,7 @@ def _make_option_pack_hash(self, type: 'Enum_OptionType', option: 'Optional[Data """ if self._type != Enum_BlockType.Packet_Block: raise ProtocolError(f'PCAP-NG: [pack_hash] option must be in Packet Block, ' - f'but found in {self._type} block.') + f'not {_block_type_name(self._type)}') if option is not None: algo_val = option.algorithm diff --git a/pcapkit/protocols/schema/misc/pcap/header.py b/pcapkit/protocols/schema/misc/pcap/header.py index 631d9d0676..07a774d4ad 100644 --- a/pcapkit/protocols/schema/misc/pcap/header.py +++ b/pcapkit/protocols/schema/misc/pcap/header.py @@ -75,7 +75,7 @@ def magic_number_callback(field: 'NumberField', packet: 'dict[str, Any]') -> 'No declared = (MAGIC_NUMBER.get(bytes(magic_number)) if isinstance(magic_number, (bytes, bytearray, memoryview)) else None) if declared is None: - raise ProtocolError('invalid magic number') + raise ProtocolError(f'PCAP: invalid magic number: {magic_number!r}') field._byteorder = declared[0] diff --git a/pcapkit/protocols/schema/misc/pcapng.py b/pcapkit/protocols/schema/misc/pcapng.py index 5a78008fb0..07b689e649 100644 --- a/pcapkit/protocols/schema/misc/pcapng.py +++ b/pcapkit/protocols/schema/misc/pcapng.py @@ -246,7 +246,7 @@ def shb_byteorder_callback(field: 'NumberField', packet: 'dict[str, Any]') -> 'N elif magic in (_SWAPPED_BYTE_ORDER_MAGIC, 0x0C000000): field._byteorder = 'little' else: - raise ProtocolError(f'unknown byteorder magic: {magic:#x}') + raise ProtocolError(f'PCAP-NG: unknown byte-order magic: {magic:#x}') packet['byteorder'] = field._byteorder @@ -1370,7 +1370,7 @@ def post_process(self, packet: 'dict[str, Any]') -> 'SectionHeaderBlock': elif magic == _SWAPPED_BYTE_ORDER_MAGIC or (magic == 0x0C000000 and self.length == GENERIC_BLOCK_MINIMUM): self.byteorder = 'little' else: - raise ProtocolError(f'unknown byteorder magic: {magic:#x}') + raise ProtocolError(f'PCAP-NG: unknown byte-order magic: {magic:#x}') return self if TYPE_CHECKING: diff --git a/tests/protocols/misc/test_capture_message_prefix_2300_2301_unit.py b/tests/protocols/misc/test_capture_message_prefix_2300_2301_unit.py new file mode 100644 index 0000000000..9d65a794df --- /dev/null +++ b/tests/protocols/misc/test_capture_message_prefix_2300_2301_unit.py @@ -0,0 +1,300 @@ +# -*- coding: utf-8 -*- +"""The capture-header and PCAP-NG option-guard messages take the house form. C.f. #2300, #2301. + +Every other message in these modules reads ``PCAP: …`` or ``PCAP-NG: …``, the +house ``: : … {value!r}`` form with no closing +full stop (#2232, #2257). These guards did not. + +#2300 -- the guards that reject a file's magic number or byte order carried no +prefix at all: :func:`~pcapkit.protocols.schema.misc.pcap.header.magic_number_callback` +raised ``invalid magic number``, naming neither the module nor the four octets +it had just read; :meth:`Header._make_magic +` raised ``unresolved +byte order`` and ``unknown byte order: 'middle'``; and PCAP-NG's +:func:`~pcapkit.protocols.schema.misc.pcapng.shb_byteorder_callback` and +:meth:`SectionHeaderBlock.post_process +` raised +``unknown byteorder magic: 0x11223344``, spelling the specification's +"Byte-Order Magic" as one word. + +#2301 -- the PCAP-NG option guards closed with a full stop, and the placement +guard formatted :attr:`PCAPNG._type ` +with a bare ``{}``. That attribute is an :class:`~enum.IntEnum`, which a bare +``{}`` renders as its value, so the message named the block type as a number: +``must be in Interface Description Block, but found in 6 block.`` rather than +``not Enhanced_Packet_Block``. + +The placement guard is reached on both passes, and ``_type`` is not the same +kind of object on each: the parse pass assigns the +:class:`~pcapkit.const.pcapng.block_type.BlockType` off the schema, while the +make pass assigns what :meth:`~pcapkit.protocols.protocol.Protocol._make_index` +returns, a plain :obj:`int` that is only :func:`~typing.cast` to the +enumeration. Reading ``.name`` off it directly therefore raises +:exc:`AttributeError` on the make pass, which is what +:func:`~pcapkit.protocols.misc.pcapng._block_type_name` exists to avoid; the +make-pass cases below are what pin that. + +Every case builds its own octets in memory and reads no capture. Classes are +imported inside each test, after +:func:`~tests._support.reimport_once_per_class`, so they match the live +:mod:`pcapkit` import. + +""" + +import io +import struct +import unittest +from typing import TYPE_CHECKING + +from tests._support import reimport_once_per_class + +if TYPE_CHECKING: + from typing import Any + + +class TestPCAPHeaderMessages(unittest.TestCase): + """#2300: PCAP's magic-number and byte-order guards.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def test_magic_number_is_prefixed_and_names_the_octets(self) -> None: + """The magic-number guard says which module rejected what.""" + from pcapkit.protocols.misc.pcap.header import Header + from pcapkit.utilities.exceptions import ProtocolError + + with self.assertRaises(ProtocolError) as ctx: + Header(io.BytesIO(b'\x00' * 24)) + self.assertEqual(str(ctx.exception), + "PCAP: invalid magic number: b'\\x00\\x00\\x00\\x00'") + + def test_magic_number_names_the_octets_it_read(self) -> None: + """The rejected octets are the ones read, not a fixed string.""" + from pcapkit.protocols.misc.pcap.header import Header + from pcapkit.utilities.exceptions import ProtocolError + + octets = b'\xde\xad\xbe\xef' + b'\x00' * 20 + with self.assertRaises(ProtocolError) as ctx: + Header(io.BytesIO(octets)) + self.assertEqual(str(ctx.exception), + "PCAP: invalid magic number: b'\\xde\\xad\\xbe\\xef'") + + def test_unresolved_byte_order_is_prefixed(self) -> None: + """Contradictory ``lilendian`` and ``bigendian`` flags name the module.""" + from pcapkit.protocols.misc.pcap.header import Header + from pcapkit.utilities.exceptions import EndianError + + with self.assertRaises(EndianError) as ctx: + Header.__new__(Header)._make_magic(lilendian=True, bigendian=True) + self.assertEqual(str(ctx.exception), 'PCAP: unresolved byte order') + + def test_unknown_byte_order_is_prefixed(self) -> None: + """An unrecognised ``byteorder`` names the module and the value.""" + from pcapkit.protocols.misc.pcap.header import Header + from pcapkit.utilities.exceptions import EndianError + + with self.assertRaises(EndianError) as ctx: + Header.__new__(Header)._make_magic(byteorder='middle') # type: ignore[arg-type] + self.assertEqual(str(ctx.exception), "PCAP: unknown byte order: 'middle'") + + +class TestPCAPNGByteOrderMessages(unittest.TestCase): + """#2300: PCAP-NG's Byte-Order Magic guards.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def test_callback_is_prefixed_and_hyphenates_byte_order(self) -> None: + """``shb_byteorder_callback`` takes the house prefix.""" + from pcapkit.protocols.schema.misc.pcapng import shb_byteorder_callback + from pcapkit.utilities.exceptions import ProtocolError + + with self.assertRaises(ProtocolError) as ctx: + shb_byteorder_callback(None, {'match': {'byteorder': 0x11223344}}) + self.assertEqual(str(ctx.exception), + 'PCAP-NG: unknown byte-order magic: 0x11223344') + + def test_post_process_is_prefixed_and_hyphenates_byte_order(self) -> None: + """The same guard on :meth:`SectionHeaderBlock.post_process` matches it. + + A Section Header Block of 16 octets has no room for the Byte-Order + Magic, so its slot holds the trailing Block Total Length -- 16 here, + which is a byte order in neither direction (#1422). + + """ + from pcapkit.protocols.misc.pcapng import PCAPNG + from pcapkit.utilities.exceptions import ProtocolError + + octets = struct.pack(' 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 test_placement_guard_names_the_block_type(self) -> None: + """The block type is named, not numbered, and the message has no full stop. + + This runs through :meth:`PCAPNG.make`, where ``_type`` is a plain + :obj:`int`, so it is also what pins the guard against the + :exc:`AttributeError` a direct ``.name`` would raise there. + + """ + 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.assertRaises(ProtocolError) as ctx: + PCAPNG(num=2, sct=1, ctx=self._context(), + type=BlockType.Interface_Statistics_Block, + block={'interface_id': 0, 'timestamp': 0, + 'options': [(OptionType.if_name, {'name': 'eth0'})]}) + self.assertEqual(str(ctx.exception), + 'PCAP-NG: [if_name] option must be in Interface Description ' + 'Block, not Interface_Statistics_Block') + + def test_placement_guard_does_not_raise_attribute_error(self) -> None: + """The make pass's plain-``int`` ``_type`` reaches the guard, not an AttributeError.""" + 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 + + seen = {} # type: dict[str, Any] + original = PCAPNG._make_option_if_name + + def spy(inner_self: 'Any', *args: 'Any', **kwargs: 'Any') -> 'Any': + seen['class'] = inner_self._type.__class__.__name__ + return original(inner_self, *args, **kwargs) + + PCAPNG._make_option_if_name = spy # type: ignore[method-assign] + try: + with self.assertRaises(ProtocolError): + PCAPNG(num=2, sct=1, ctx=self._context(), + type=BlockType.Interface_Statistics_Block, + block={'interface_id': 0, 'timestamp': 0, + 'options': [(OptionType.if_name, {'name': 'eth0'})]}) + finally: + PCAPNG._make_option_if_name = original # type: ignore[method-assign] + + # The guard ran with a plain ``int``, which has no ``.name``. + self.assertEqual(seen['class'], 'int') + + def test_block_type_name_takes_both_an_enum_and_a_plain_int(self) -> None: + """The helper covers both passes' idea of ``_type``. + + The parse pass assigns the enumeration member off the schema and the + make pass a plain :obj:`int`, and the guard is shared, so the helper + has to name either. Reaching a placement guard through a *parse* is + not possible -- an option code is resolved in the namespace of the + block being parsed, so code 2 in an Interface Statistics Block is + ``isb_starttime`` and never ``if_name`` -- which is why this is + checked on the helper rather than through a capture. + + """ + from pcapkit.const.pcapng.block_type import BlockType + from pcapkit.protocols.misc.pcapng import _block_type_name + + self.assertEqual(_block_type_name(BlockType.Enhanced_Packet_Block), + 'Enhanced_Packet_Block') + self.assertEqual(_block_type_name(6), 'Enhanced_Packet_Block') # type: ignore[arg-type] + self.assertEqual(_block_type_name(BlockType.Interface_Description_Block), + 'Interface_Description_Block') + self.assertEqual(_block_type_name(1), 'Interface_Description_Block') # type: ignore[arg-type] + + def test_only_one_guard_has_no_full_stop(self) -> None: + """A repeated "only one" option reports the count in the house form.""" + 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.assertRaises(ProtocolError) as ctx: + 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'})]}) + self.assertEqual(str(ctx.exception), + 'PCAP-NG: [if_name] option must be only one, not 2') + + def test_if_tsresol_guard_has_no_full_stop(self) -> None: + """The ``if_tsresol`` resolution guard takes the same ``not …`` form.""" + 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.assertRaises(ProtocolError) as ctx: + PCAPNG(num=2, sct=1, ctx=self._context(), + type=BlockType.Interface_Description_Block, + block={'linktype': 1, 'snaplen': 65535, + 'options': [(OptionType.if_tsresol, {'resolution': 3})]}) + self.assertEqual(str(ctx.exception), + 'PCAP-NG: [if_tsresol] option resolution must be power ' + 'of 10 or 2, not 3') + + def test_if_tzone_guard_has_no_full_stop(self) -> None: + """The ``if_tzone`` timezone guard takes the same ``not …`` form.""" + 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.assertRaises(ProtocolError) as ctx: + PCAPNG(num=2, sct=1, ctx=self._context(), + type=BlockType.Interface_Description_Block, + block={'linktype': 1, 'snaplen': 65535, + 'options': [(OptionType.if_tzone, {'tzone': 'UTC'})]}) + self.assertEqual(str(ctx.exception), + 'PCAP-NG: [if_tzone] option timezone must be int, ' + 'timedelta or timezone, not str') + + def test_no_option_guard_message_ends_in_a_full_stop(self) -> None: + """#2301: the whole module is free of the full-stop form, not just the guards hit above.""" + import re + + from pcapkit.protocols.misc import pcapng + + with open(pcapng.__file__, encoding='utf-8') as file: + source = file.read() + + # A message fragment on its own line, closing the call and ending in a + # full stop -- the shape every guard in #2301 had. + offenders = re.findall(r"^\s+f?'[^']*\.'\)", source, re.MULTILINE) + self.assertEqual(offenders, []) + + def test_no_guard_formats_the_block_type_with_a_bare_brace(self) -> None: + """#2301: ``{self._type}`` in a message would render the block type as a number.""" + from pcapkit.protocols.misc import pcapng + + with open(pcapng.__file__, encoding='utf-8') as file: + source = file.read() + + self.assertNotIn('but found in {self._type} block', source) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/protocols/misc/test_pcapng_raw_option_byteorder_2353_unit.py b/tests/protocols/misc/test_pcapng_raw_option_byteorder_2353_unit.py index c0a3cdcb50..4db653fe97 100644 --- a/tests/protocols/misc/test_pcapng_raw_option_byteorder_2353_unit.py +++ b/tests/protocols/misc/test_pcapng_raw_option_byteorder_2353_unit.py @@ -186,7 +186,7 @@ def test_a_raw_option_code_is_read_in_the_section_byte_order(self) -> None: with self.subTest(order=order): context = self._context(order) with self.assertRaisesRegex(ProtocolError, - r'\[if_name\] option must be only one, but 2 found'): + r'\[if_name\] option must be only one, not 2'): PCAPNG(num=2, sct=1, ctx=context, type=BlockType.Interface_Description_Block, block={'linktype': 1, 'snaplen': 0xFFFF, @@ -209,7 +209,7 @@ def test_a_raw_option_code_is_read_in_the_packet_byte_order(self) -> None: for order in ('little', 'big'): # type: Literal['little', 'big'] with self.subTest(order=order): with self.assertRaisesRegex(ProtocolError, - r'\[if_name\] option must be only one, but 2 found'): + r'\[if_name\] option must be only one, not 2'): PCAPNG(num=2, sct=1, ctx=None, type=BlockType.Interface_Description_Block, block={'linktype': 1, 'snaplen': 0xFFFF, @@ -245,7 +245,7 @@ def build(order: 'Literal["little", "big"]') -> 'Any': (OptionType.if_name, {'name': 'eth1'})]}) with self.assertRaisesRegex(ProtocolError, - r'\[if_name\] option must be only one, but 2 found'): + r'\[if_name\] option must be only one, not 2'): build(sys.byteorder) block = build(other) @@ -277,7 +277,7 @@ def test_a_bound_section_outranks_the_byte_order_the_packet_data_declares(self) int(BlockType.Interface_Description_Block).to_bytes(4, order)) with self.assertRaisesRegex(ProtocolError, - r'\[if_name\] option must be only one, but 2 found'): + r'\[if_name\] option must be only one, not 2'): PCAPNG(num=2, sct=1, ctx=self._context(order), type=BlockType.Interface_Description_Block, block={'linktype': 1, 'snaplen': 0xFFFF, @@ -313,7 +313,7 @@ def test_a_nested_packet_byte_order_is_read_one_level_up(self) -> None: int(BlockType.Interface_Description_Block).to_bytes(4, order)) with self.assertRaisesRegex(ProtocolError, - r'\[if_name\] option must be only one, but 2 found'): + r'\[if_name\] option must be only one, not 2'): PCAPNG(num=2, sct=1, ctx=None, type=BlockType.Interface_Description_Block, block={'linktype': 1, 'snaplen': 0xFFFF, diff --git a/tests/protocols/misc/test_pcapng_roundtrip_unit.py b/tests/protocols/misc/test_pcapng_roundtrip_unit.py index 06956f6a49..68cc0acb82 100644 --- a/tests/protocols/misc/test_pcapng_roundtrip_unit.py +++ b/tests/protocols/misc/test_pcapng_roundtrip_unit.py @@ -127,7 +127,7 @@ def test_a_repeated_only_one_option_is_still_rejected(self) -> None: 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'): + with self.assertRaisesRegex(ProtocolError, r'\[if_name\] option must be only one, not 2'): PCAPNG(num=2, sct=1, ctx=self._context(), type=BlockType.Interface_Description_Block, block={'linktype': 1, 'snaplen': 65535, 'options': [(OptionType.if_name, {'name': 'eth0'}), diff --git a/tests/protocols/misc/test_pcapng_short_shb_idb_1422_unit.py b/tests/protocols/misc/test_pcapng_short_shb_idb_1422_unit.py index a0a93f0ae2..75213558e2 100644 --- a/tests/protocols/misc/test_pcapng_short_shb_idb_1422_unit.py +++ b/tests/protocols/misc/test_pcapng_short_shb_idb_1422_unit.py @@ -161,7 +161,8 @@ def test_shb_without_magic_needs_a_length_of_12(self) -> None: from pcapkit.utilities.exceptions import ProtocolError octets = struct.pack(' None: