From 016b0f3debccad9a24f8a5fad6411e636d4834d7 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Mon, 26 Oct 2026 14:26:27 -0400 Subject: [PATCH] docs(changelog): seventh whole-repo pass over changelog (#1103) - #876 item 3: state why the no-else-raise pragma stays, not "the belief" - #652: the flagless HTTP/2 dump reads "Flags::0 [0]", as #648 renders it - #2172: section_context raises only on a block holding no section - add the missing #2222/#2232 MAC/EUI maker entry; cite #2257 on ARP - add the #1055/#2244 engine-tests and required-checks entry that #849's and #856's job descriptions now need - #800 names HTTP._guess_version, where the method lives; #631 states the old MANIFEST.in in the past tense - drop mentions of never-released names (pcapkit.utilities.timestamp, a test constant) - citation range runs to #2257; CHANGELOG.md regenerated --- CHANGELOG.md | 20 ++++++------ docs/source/changelog/1.5.0.rst | 57 ++++++++++++++++++++------------- 2 files changed, 46 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dcdef608e..d76e441b02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## 1.5.0 -- unreleased -The largest release since 1.0, and the first recorded here as it happened rather than reconstructed. New extraction engines, ESP with payload decryption, SCTP and NGAP over SCTP, a library logger that no longer hijacks the consumer's, and a defect programme run through the issue tracker, citing issues and pull requests from [#340](https://github.com/JarryShaw/PyPCAPKit/pull/340) to [#2205](https://github.com/JarryShaw/PyPCAPKit/pull/2205). +The largest release since 1.0, and the first recorded here as it happened rather than reconstructed. New extraction engines, ESP with payload decryption, SCTP and NGAP over SCTP, a library logger that no longer hijacks the consumer's, and a defect programme run through the issue tracker, citing issues and pull requests from [#340](https://github.com/JarryShaw/PyPCAPKit/pull/340) to [#2257](https://github.com/JarryShaw/PyPCAPKit/pull/2257). Preceded by `1.5.0a1` (2026-09-15), `1.5.0b1` to `1.5.0b3` (all 2026-09-19) and `1.5.0b4` to `1.5.0b14` (2026-09-20 to 2026-10-22), dated in UTC and all published as prereleases and so resolved only by `pip install --pre`. `1.5.0b1` half-shipped: the tag, the GitHub release and the Conda deployments landed, but PyPI rejected the wheel because `twine check` found a Sphinx-only `:mod:` role in `README.rst`, which `pyproject.toml` declares as the dynamic long description. `1.5.0b2` reshipped it -- an existing version cannot republish -- and `1.5.0b3` followed the CI change that stops a TestPyPI outage from costing a release its wheels ([#497](https://github.com/JarryShaw/PyPCAPKit/pull/497), [#498](https://github.com/JarryShaw/PyPCAPKit/pull/498)). @@ -11,7 +11,7 @@ Preceded by `1.5.0a1` (2026-09-15), `1.5.0b1` to `1.5.0b3` (all 2026-09-19) and - **a breaking change to** `pcapkit.const.reg.apptype`. `AppType` becomes a memberless base over one registry per transport protocol -- `TCP`, `UDP`, `SCTP` and `DCCP` -- because `aenum` refuses to subclass an enumeration that has members. `isinstance(TCP.http, AppType)` still holds and `AppType.get(port, proto=...)` proxies to the registry, so nothing under `pcapkit.protocols` changes. **The rows IANA assigns no port, and those it assigns no transport protocol, stop being members**; they become list-table docstrings on the registry that owns them. `__members_proto__` is now a per-registry `MultiDict` keyed on port, so every service IANA registers on port 80 survives. `get()` answers with the canonical service, curated from `/etc/services` because IANA names no precedence, and `get_all()` reaches the aliases ([#754](https://github.com/JarryShaw/PyPCAPKit/pull/754)). - the `%`-formatted `raise ValueError(...)` calls in the generated `pcapkit.const.reg.apptype.apptype`, left by [#759](https://github.com/JarryShaw/PyPCAPKit/issues/759), are f-strings ([#792](https://github.com/JarryShaw/PyPCAPKit/issues/792)). The remainder followed: the `_missing_` guards in `pcapkit.vendor.tcp.flags`, `.ftp.command` and `.http.method`, `AppType`'s `__new__`/`__repr__`/`__str__`, and the `extend_enum` fallbacks in `get()` and in the span-handling tail. The `consider-using-f-string` disable drops from `tcp.flags` and `reg.apptype.apptype` and stays on `ftp.command` and `http.method`, whose `__repr__` still used `%`. Edited in the vendor templates and applied to the generated const files by hand, since a crawl would fetch IANA's live registry and rewrite unrelated rows ([#798](https://github.com/JarryShaw/PyPCAPKit/issues/798)). - the `__repr__` methods [#798](https://github.com/JarryShaw/PyPCAPKit/issues/798) left on `%` (in `pcapkit/const/ftp/command.py` and `pcapkit/const/http/method.py`) are f-strings, so the disable drops from both files and both `pcapkit/vendor/` templates. The other bespoke templates (`{const,vendor}/ftp/return_code.py`, `{const,vendor}/http/status_code.py`, `vendor/http/frame.py`) and `vendor/default.py` still carry it, so [#804](https://github.com/JarryShaw/PyPCAPKit/issues/804)'s claim that nothing in `pcapkit/{const,vendor}/{ftp,http}/` needs it holds for this pair only; `repr()` output is unchanged ([#804](https://github.com/JarryShaw/PyPCAPKit/issues/804)). -- `test_const_enum_no_mint.py`'s `is_manufactured()` exemption for the hex-suffixed names [#878](https://github.com/JarryShaw/PyPCAPKit/pull/878) (below) preserves moves from a per-*file* to a per-*name-shape* one. The per-file `HEX_SUFFIXED_NAME_EXEMPT_PATHS` was shape-blind: rewriting a branch to `'Xyplex_%d' % value` -- exactly the value-derived shape the predicate exists to flag -- left the sweep green. The check now exempts only a `'..._0x%s' % hex(value)[2:].upper().zfill(4)` expression, and the exact `exempt_hits == 53` bound stays. Test-only. Closes [#879](https://github.com/JarryShaw/PyPCAPKit/issues/879) ([#881](https://github.com/JarryShaw/PyPCAPKit/pull/881)). +- `test_const_enum_no_mint.py`'s `is_manufactured()` exemption for the hex-suffixed names [#878](https://github.com/JarryShaw/PyPCAPKit/pull/878) (below) preserves moves from a per-*file* to a per-*name-shape* one. The per-file exemption was shape-blind: rewriting a branch to `'Xyplex_%d' % value` -- exactly the value-derived shape the predicate exists to flag -- left the sweep green. The check now exempts only a `'..._0x%s' % hex(value)[2:].upper().zfill(4)` expression, and the exact `exempt_hits == 53` bound stays. Test-only. Closes [#879](https://github.com/JarryShaw/PyPCAPKit/issues/879) ([#881](https://github.com/JarryShaw/PyPCAPKit/pull/881)). - **a breaking change to** `AppType.proto`: it is the single transport of the registry the member lives in, not every transport IANA assigned the service, and `register_apptype` takes its transports as varargs ([#806](https://github.com/JarryShaw/PyPCAPKit/issues/806), [#815](https://github.com/JarryShaw/PyPCAPKit/pull/815)). Aliases show in `repr()` and `str()`, and can be registered per transport ([#807](https://github.com/JarryShaw/PyPCAPKit/issues/807), [#837](https://github.com/JarryShaw/PyPCAPKit/pull/837)). - **a breaking change to** `TransportProtocol`: its base moves from `aenum.IntFlag` to `aenum.IntEnum`, so `|` between two members gives a plain `int` rather than a composite member ([#808](https://github.com/JarryShaw/PyPCAPKit/issues/808), [#836](https://github.com/JarryShaw/PyPCAPKit/pull/836)). - **a breaking change to** `TLSKeyLabel`: the hand-written class in `pcapkit.protocols.misc.pcapng` becomes the generated `pcapkit.const.pcapng.tls_key_label.TLSKeyLabel` (`PCAPNG_TLSKeyLabel` in `pcapkit.const.pcapng`), crawled from the IANA registry [RFC 9850](https://datatracker.ietf.org/doc/html/rfc9850) creates ([#886](https://github.com/JarryShaw/PyPCAPKit/issues/886), [#890](https://github.com/JarryShaw/PyPCAPKit/pull/890)), and the old path is removed ([#1744](https://github.com/JarryShaw/PyPCAPKit/issues/1744), [#1754](https://github.com/JarryShaw/PyPCAPKit/pull/1754)). @@ -224,7 +224,7 @@ Layer-limited extraction: `layer='link'` no longer sets the termination flag on - **a breaking change to** the HOPOPT, IPv6-Opts and Mobility Header padding: two octets are one `PadN` with no data (`01 00`) rather than two `Pad1` options, as [RFC 8200 Section 4.2](https://datatracker.ietf.org/doc/html/rfc8200#section-4.2) and [RFC 6275 Section 6.2.2](https://datatracker.ietf.org/doc/html/rfc6275#section-6.2.2) ask. Only a freshly built header with exactly two octets of padding changes; a parsed one that carried two `Pad1` options still rebuilds byte for byte ([#1793](https://github.com/JarryShaw/PyPCAPKit/issues/1793), [#1812](https://github.com/JarryShaw/PyPCAPKit/pull/1812)). - **a breaking change to** `NTPTimestamp`: it and `PMIPv6Timestamp` are `Info` classes in `pcapkit.protocols.data.internet.mh` rather than tuples, and `NTPTimestamp` keeps no alias at its 1.4.x home in `pcapkit.protocols.internet.mh`. As Mappings they no longer index, unpack, compare or hash as a tuple does; `.seconds` and `.fraction` still work, and the dumpers write both as keys ([#1886](https://github.com/JarryShaw/PyPCAPKit/issues/1886), [#1887](https://github.com/JarryShaw/PyPCAPKit/pull/1887)). - **a breaking change to** rebuilding from `to_dict()`: the payload is dispatched only from the `__next_name__` and `__next_type__` keys, and the IPv6 extension-header chain only from `__exthdr__`, never from key order, which on PyPy follows an earlier instance's attribute layout and so picked the wrong layer. `to_dict()`, iteration and `len()` carry the dunder keys a new `Info.__included__` names, and `Info.__excluded__` only keeps a key out of `repr`, `str` and the dumps. A hand-written dict needs those keys, `__short_read__` among them, to rebuild, and a custom `Info` lists its own dunder keys in `__included__` ([#2003](https://github.com/JarryShaw/PyPCAPKit/issues/2003), [#2014](https://github.com/JarryShaw/PyPCAPKit/pull/2014)). -- **a breaking change to** `PCAPNG.context`: the block's section `Context`, which it returned from 1.0.0 through 1.4.x, is `PCAPNG.section_context`, with no alias, and `context` is the caller supplied `ContextRegistry` every protocol inherits from `ProtocolBase`, on a Section Header Block too. `PCAPNG.section_context` raises `UnsupportedCall` on a Section Header Block, as `context` did. Code reading `block.context` for the section reads `block.section_context` ([#2172](https://github.com/JarryShaw/PyPCAPKit/issues/2172), [#2205](https://github.com/JarryShaw/PyPCAPKit/pull/2205)). +- **a breaking change to** `PCAPNG.context`: the block's section `Context`, which it returned from 1.0.0 through 1.4.x, is `PCAPNG.section_context`, with no alias, and `context` is the caller-supplied `ContextRegistry` every protocol inherits from `ProtocolBase`, on a Section Header Block too. `PCAPNG.section_context` raises `UnsupportedCall` where `context` did: on a block that holds no section, such as a Section Header Block the `PCAPNG` engine has not assigned the section it opens. Code reading `block.context` for the section reads `block.section_context` ([#2172](https://github.com/JarryShaw/PyPCAPKit/issues/2172), [#2205](https://github.com/JarryShaw/PyPCAPKit/pull/2205)). #### Fixed @@ -257,7 +257,7 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r - **a breaking change to a public attribute.** `Frame.len` and `Frame.cap_len` were filled from *opposite* wire fields depending on the container format, so `frame.len` meant the captured length out of a `.pcap` and the on-wire length out of a `.pcapng`. The PCAP reader moved to match PCAP-NG: `len` is the on-wire length (the record header's `orig_len`) and `cap_len` the octets stored (`incl_len`). **Code reading** `frame.len` **or** `frame.cap_len` **from a** `.pcap` **gets the other field's value than before**, and for a truncated frame that is a different number rather than a relabelling. Seniority did not settle it: the PCAP spelling is the *older* (`c43892af`, 2022-01-11) and PCAP-NG's arrived in `25f216f4` (2023-04-27), both internally consistent. The *names* settle it, being Wireshark's: `frame.len` is "Frame length on the wire" and `frame.cap_len` "Frame length stored into the capture file", and `frame.len_lt_caplen` expert info flags `frame_len < cap_len` as malformed, which it could not be if `len` were the smaller, captured one. `Frame.read()` hands `_decode_next_layer` the octets *present*, now `frame.cap_len`; handing over the on-wire length would be the declared-length-exceeds-available-octets fault of [#554](https://github.com/JarryShaw/PyPCAPKit/issues/554), [#573](https://github.com/JarryShaw/PyPCAPKit/issues/573) and [#594](https://github.com/JarryShaw/PyPCAPKit/issues/594) on every snapped frame. The dumpers are untouched. The two lengths differ only for a snapshot-cut frame, and no such fixture existed until [#614](https://github.com/JarryShaw/PyPCAPKit/pull/614) ([#618](https://github.com/JarryShaw/PyPCAPKit/issues/618)). - the HIP `PUZZLE` and `SOLUTION` builders derived wire-format values from the payload value instead of the data model, and each was wrong. **This changes the** `PuzzleParameter` **and** `SolutionParameter` **data models and the octets both parameters emit.** The width of `Random #I` and `Puzzle solution #J` came from `int.bit_length()` alone, so every leading zero octet was dropped on re-serialisation: a `SOLUTION` read with `Length = 20` rebuilt as `Length = 6`, a `PUZZLE` with `Length = 12` as `Length = 5`, silently. That width is `RHASH_len / 8` octets [[RFC 7401 Section 5.2.4](https://datatracker.ietf.org/doc/html/rfc7401#section-5.2.4), [RFC 7401 Section 5.2.5](https://datatracker.ietf.org/doc/html/rfc7401#section-5.2.5)], a property of the Responder's HIT Suite, so both data models now carry it as `rhash_len` and both builders prefer it; a new `rhash_len=` keyword states it for a build from scratch, which under HIPv2 is the only place it can come from, since `RSA,DSA/SHA-256` is the REQUIRED HIT Suite [[RFC 7401 Section 5.2.10](https://datatracker.ietf.org/doc/html/rfc7401#section-5.2.10)] and makes the field 32 octets rather than 8. On the mandatory path about one parameter in 256 has a zero top octet and lost it ([#653](https://github.com/JarryShaw/PyPCAPKit/issues/653)). `SOLUTION`'s second contents octet is `Reserved`, "zero when sent, ignored when received" [[RFC 7401 Section 5.2.5](https://datatracker.ietf.org/doc/html/rfc7401#section-5.2.5), and [RFC 5201 Section 5.2.5](https://datatracker.ietf.org/doc/html/rfc5201#section-5.2.5) identically], not the `Lifetime` that only [RFC 7401 Section 5.2.4](https://datatracker.ietf.org/doc/html/rfc7401#section-5.2.4) defines, which pcapkit encoded there as `2^(value - 32)` seconds. It wrote `0x20`, `0x21`, `0x25` or `0x2b` into a field the RFC requires to be zero, and could not write that zero: a conformant `SOLUTION` with `Reserved` `0x00` parsed to `timedelta(0)` and could not be re-serialised, escaping a bare `ValueError` from `math.log2(0.0)` that was not a `BaseError`. The field is renamed `reserved`, defaults to zero, and is carried verbatim across a round trip ([#654](https://github.com/JarryShaw/PyPCAPKit/issues/654)). And neither builder read its own `version` keyword, so `version=1` and `version=2` computed identical lengths, where [RFC 5201 Section 5.2.4](https://datatracker.ietf.org/doc/html/rfc5201#section-5.2.4) and [RFC 5201 Section 5.2.5](https://datatracker.ietf.org/doc/html/rfc5201#section-5.2.5) state both fields as literally 8 bytes and `Length` as literally 12 and 20. Under HIPv1 each builder accepted only a `bit_length()` of 57..64 and built, for anything narrower, a parameter this library's reader rejects -- the shape of [#608](https://github.com/JarryShaw/PyPCAPKit/issues/608), in the `PUZZLE` builder that [#629](https://github.com/JarryShaw/PyPCAPKit/pull/629) never opened ([#655](https://github.com/JarryShaw/PyPCAPKit/issues/655)). The remaining `math.log2` sites, in `PUZZLE` where a lifetime is real, raise `ProtocolError` (`(BaseError, ValueError)`, so a caller written around the bare exception still catches it). Migration: `SolutionParameter.lifetime` is now `reserved` and an `int` rather than a `timedelta`; `PuzzleParameter` and `SolutionParameter` gained a required `rhash_len`; and `_make_param_solution` no longer takes `lifetime=`. Each was reachable end to end only once [#608](https://github.com/JarryShaw/PyPCAPKit/issues/608) was fixed in [#629](https://github.com/JarryShaw/PyPCAPKit/pull/629), which removed the parity guard that had failed these rebuilds loudly; the round trip then stopped raising and started quietly emitting a different parameter, which is why they were fixed together ([#653](https://github.com/JarryShaw/PyPCAPKit/issues/653), [#654](https://github.com/JarryShaw/PyPCAPKit/issues/654), [#655](https://github.com/JarryShaw/PyPCAPKit/issues/655)). - every HIP parameter pcapkit emitted was `4 (mod 8)` octets long, because the padding aligned the *contents* rather than the record. Corrected for **every HIP parameter but** `LOCATOR_SET`, excluded below. **This changes the octets those parameters write, and the** `length` **their data models report.** [RFC 7401 Section 5.2.1](https://datatracker.ietf.org/doc/html/rfc7401#section-5.2.1) requires every TLV parameter to have "a length (that includes the Type and Length fields), which is a multiple of 8 bytes" and gives `Total Length = 11 + Length - (Length + 3) % 8`; every padding site computed `(8 - (Length % 8)) % 8`, which aligns the contents alone. For every `Length` from 0 to 63 the result was never a multiple of eight and never the RFC's value, erring both ways: at `Length = 4` (a whole `SEQ`, which [RFC 7401 Section 5.3.5](https://datatracker.ietf.org/doc/html/rfc7401#section-5.3.5) puts on every `UPDATE`) pcapkit appended four octets that must not be there, and at `Length = 8` it appended none and went out four short. A conformant peer lands mid-parameter; pcapkit did not, because its reader consumed the same wrong count its writer wrote, which is why no round-trip test could see it and why the fix is asserted against the RFC's arithmetic. Every site but `LOCATOR_SET`'s (the `PaddingField` callbacks in `pcapkit/protocols/schema/internet/hip.py` and the record lengths in `pcapkit/protocols/internet/hip.py`) now goes through one `parameter_total_len` and one `parameter_padding_len` ([#651](https://github.com/JarryShaw/PyPCAPKit/issues/651)). `LOCATOR_SET` kept the old expression because defects there cancelled exactly: its padding callback never received the parameter's `len` (the nested `Locator` schemas share a packet context whose own `len` shadowed it, so it saw 4 for an IPv6 locator), and the parameter's `len` was written in 4-octet units where the RFC's `Length` is a byte count; the result, `4 + 24n + 4`, equalled the RFC total `24n + 8`. [#679](https://github.com/JarryShaw/PyPCAPKit/issues/679) later fixed both sites. `EncryptedParameter`'s `data` length callback is fixed in the same change: it subtracted the sixteen `iv` octets but not the four `reserved` ones, which cancelled the padding's four at `Length % 8` in `{0, 5, 6, 7}`, so correcting the padding alone would have made `ENCRYPTED` four octets too long at all eight residues. `HIP.make()`'s `len = total_length // 8 + 4` is not part of the defect: [RFC 7401 Section 5.1.3](https://datatracker.ietf.org/doc/html/rfc7401#section-5.1.3) defines Header Length in 8-byte units excluding the first 8 bytes, so the floor division is exact once each parameter is a multiple of eight. Migration: a `SEQ` parameter's `length` is now 8 where it was 12, and the other parameters move likewise, so code comparing stored output byte for byte or asserting on `Data_*Parameter.length` sees the RFC's values. `LOCATOR_SET` is unchanged in both respects until [#679](https://github.com/JarryShaw/PyPCAPKit/issues/679) (below). `examples/generators/options.py`'s `HIP_COPIES` stayed at two for `R1_COUNTER`'s four-octet `counter` where [RFC 7401 Section 5.2.3](https://datatracker.ietf.org/doc/html/rfc7401#section-5.2.3) requires eight, which this defect had masked, until [#672](https://github.com/JarryShaw/PyPCAPKit/issues/672) widened `counter` and [#679](https://github.com/JarryShaw/PyPCAPKit/issues/679) fixed `LOCATOR_SET`'s `Length` unit, after which [#689](https://github.com/JarryShaw/PyPCAPKit/issues/689) dropped `HIP_COPIES` to one ([#651](https://github.com/JarryShaw/PyPCAPKit/issues/651)). -- HTTP/2 flag defects on each side of a round trip. **This changes the octets a reconstructed DATA frame emits, and the dumped value of a flagless frame's flags.** `_make_http_data` was the only `_make_http_*` method that never read `frame.flags`, so a DATA frame parsed with `END_STREAM` set rebuilt with the bit clear (flags octet `0x01` to `0x00`): a well-formed frame saying the stream continues where the capture said it ended [[RFC 9113 Section 6.1](https://datatracker.ietf.org/doc/html/rfc9113#section-6.1)]. `PADDED` was re-derived from `pad_len`, and the sibling builders already restored theirs, so this was a missing read-back ([#652](https://github.com/JarryShaw/PyPCAPKit/issues/652)). Separately, `FrameType.post_process()` seeded its flag accumulator with a bare `0`, which `|=` promotes only as a side effect, so a frame with **no** bit set left `__flags__` a plain `int` where the schema and data model declare a `Flags`, and `END_STREAM in __value__` raised `TypeError: argument of type 'int' is not a container or iterable` rather than answering `False`. That is the common case: a `0x00` flags octet is every opening `SETTINGS`, every non-final `DATA` and every non-ACK `PING`. It now seeds `self.Flags(0)`, as the construct path already did at each of its sites. In the dump, `__value__` was a JSON *number* for a flagless frame and a *string* for every other frame in the same capture; it is consistently a string now, `"Flags::None [0]"` where it read `0`, the same change [#616](https://github.com/JarryShaw/PyPCAPKit/issues/616) made to TCP's `connection`. The seed is guarded because `FrameType.Flags` declares no members and, from Python 3.11, a memberless `enum.Flag` subclass refuses `Flags(0)`, so the frame schemas that inherit it (`UnassignedFrame`, `PriorityFrame`, `RSTStreamFrame`, `GoawayFrame`, `WindowUpdateFrame`) keep the plain `int`, which nothing observes since each passes `flags=None`. A DATA round trip is **still** lossy for an unrelated reason found while measuring: payload length callbacks are mis-parenthesised, so an unpadded `DATA`, `HEADERS` or `PUSH_PROMISE` frame parses with its whole payload dropped ([#668](https://github.com/JarryShaw/PyPCAPKit/issues/668), below) ([#652](https://github.com/JarryShaw/PyPCAPKit/issues/652), [#650](https://github.com/JarryShaw/PyPCAPKit/issues/650)). +- HTTP/2 flag defects on each side of a round trip. **This changes the octets a reconstructed DATA frame emits, and the dumped value of a flagless frame's flags.** `_make_http_data` was the only `_make_http_*` method that never read `frame.flags`, so a DATA frame parsed with `END_STREAM` set rebuilt with the bit clear (flags octet `0x01` to `0x00`): a well-formed frame saying the stream continues where the capture said it ended [[RFC 9113 Section 6.1](https://datatracker.ietf.org/doc/html/rfc9113#section-6.1)]. `PADDED` was re-derived from `pad_len`, and the sibling builders already restored theirs, so this was a missing read-back ([#652](https://github.com/JarryShaw/PyPCAPKit/issues/652)). Separately, `FrameType.post_process()` seeded its flag accumulator with a bare `0`, which `|=` promotes only as a side effect, so a frame with **no** bit set left `__flags__` a plain `int` where the schema and data model declare a `Flags`, and `END_STREAM in __value__` raised `TypeError: argument of type 'int' is not a container or iterable` rather than answering `False`. That is the common case: a `0x00` flags octet is every opening `SETTINGS`, every non-final `DATA` and every non-ACK `PING`. It now seeds `self.Flags(0)`, as the construct path already did at each of its sites. In the dump, `__value__` was a JSON *number* for a flagless frame and a *string* for every other frame in the same capture; it is consistently a string now, `"Flags::0 [0]"` (the spelling the `pcapkit.dumpkit` fix above gives a nameless member) where it read `0`, the same change [#616](https://github.com/JarryShaw/PyPCAPKit/issues/616) made to TCP's `connection`. The seed is guarded because `FrameType.Flags` declares no members and, from Python 3.11, a memberless `enum.Flag` subclass refuses `Flags(0)`, so the frame schemas that inherit it (`UnassignedFrame`, `PriorityFrame`, `RSTStreamFrame`, `GoawayFrame`, `WindowUpdateFrame`) keep the plain `int`, which nothing observes since each passes `flags=None`. A DATA round trip is **still** lossy for an unrelated reason found while measuring: payload length callbacks are mis-parenthesised, so an unpadded `DATA`, `HEADERS` or `PUSH_PROMISE` frame parses with its whole payload dropped ([#668](https://github.com/JarryShaw/PyPCAPKit/issues/668), below) ([#652](https://github.com/JarryShaw/PyPCAPKit/issues/652), [#650](https://github.com/JarryShaw/PyPCAPKit/issues/650)). - MPTCP error messages carried a doubled separator, rendering as `TCP: : [OptNo 30] 1: invalid flags combination`. Cosmetic: the exception type, option number and subtype were right and nothing parses these strings, but an empty field reads as a value that failed to interpolate. The sites were `_read_mptcp_join` and `_make_mptcp_join` for the invalid flag combination, and both guards of `_make_mptcp_dss` for missing required fields; every other message in the file was already spelled correctly, and none is doubled now. Pre-existing since 2023-04-10. The new test asserts each message in full, since the existing `assertIn` on the body passes either side of the change ([#649](https://github.com/JarryShaw/PyPCAPKit/issues/649)). - names appeared in string annotations their own module never imported, so a type checker or documentation build could not resolve them while every test passed: `Any` in `pcapkit/utilities/logging.py`, and in `pcapkit/protocols/schema/internet/ipv6_route.py` both `Protocol` in a `payload:` stub and `Optional` inside a `typing.cast`. Each is now in its module's `TYPE_CHECKING` block, `Protocol` spelled `ProtocolBase as Protocol` as the sibling schema modules already do. The `cast` case is the one worth naming: `typing.cast` never evaluates its first argument, so no test that runs the line can see a bad name in it. Nothing resolves at runtime that did not before (`TYPE_CHECKING` is `False`); making these annotations resolve at runtime is a separate decision about the idiom. A new `tests/project/test_annotation_names.py` pins the invariant without a type checker: it resolves every string annotation in the package against the names its own module binds, following nested forward references such as `'list["Nested"]'` and treating `Literal` members and `Annotated` metadata as values; without the fix, it reports exactly those findings. The same change corrects the `MANIFEST.in` comment asserting that `include README.md` was the only thing putting the README in a source distribution and that an sdist without it could not be installed; both halves are false, and the [#619](https://github.com/JarryShaw/PyPCAPKit/pull/619) entry above carried the same claim and is corrected with it ([#642](https://github.com/JarryShaw/PyPCAPKit/issues/642)). - 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)). @@ -271,7 +271,7 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r - `SystemdJournalExportBlock.post_process()` skipped a binary field's one trailing newline with a bare `entry_data.read()`, which reads to **EOF**: the next `readline()` returned `b''` and ended the entry, discarding every field behind the first binary one. The skip is bound to `entry_data.read(1)`, and a value not followed by its newline raises `ProtocolError` rather than resynchronising at the wrong offset, so the block's journal data is kept as captured, as one `bytes` item, with a `ProtocolWarning` ([#1406](https://github.com/JarryShaw/PyPCAPKit/issues/1406)). Without the fix, `test_journal_fields_following_a_binary_field_are_not_discarded` fails with `MissingKeyError: 'SECOND'` ([#704](https://github.com/JarryShaw/PyPCAPKit/issues/704)). - the same block's entries were split on a bare `b'\n\n'`, which shreds a binary field whose value contains that pair. The entries are walked instead, each binary value counted out by its length prefix; a bad terminator or length raises `ProtocolError`, so the block's journal data is kept as captured, with a `ProtocolWarning` ([#1406](https://github.com/JarryShaw/PyPCAPKit/issues/1406)). Another gap closed in the same change: a trailing separator on the block's last octet was indistinguishable from EOF and swallowed, so a rebuild wrote `length` one octet short ([#723](https://github.com/JarryShaw/PyPCAPKit/issues/723)). - **a breaking change to** `.get()`-backed enum fields: TCP/UDP/SCTP's `PortEnumField` and PCAP-NG's `OptionEnumField` each minted a member through `aenum.extend_enum` for every value no row or documented span covers (111 calls on `http.pcap`, all ephemeral ports). Each now peeks at what `.get()` would consult and call it only once the value resolves; a genuine miss gets `EnumField._unregistered_member()`, a real member of the same registry built through its storage base's `__new__`, so `isinstance` holds and the lookup table stops growing. Riding along: PCAP-NG option namespaces stop cross-contaminating: once a code was minted under `opt`, `OptionType.get()` merged `opt` over the requested namespace and most other namespaces answered `opt_unknown` for it; each now gets its own `_unknown`. **Breaking** because [#575](https://github.com/JarryShaw/PyPCAPKit/issues/575) asked for byte-identical output across the sample captures, as [#420](https://github.com/JarryShaw/PyPCAPKit/pull/420) and [#427](https://github.com/JarryShaw/PyPCAPKit/pull/427) did, and this deliberately misses that bar: some sample captures change, entirely from the new `` rendering plus the PLIST escaping above; normalising both leaves them byte-identical. So that a resolved unassigned member still pickles, `_unregistered_member` installs a `__reduce_ex__` rebuilding an equivalent unregistered member ([#575](https://github.com/JarryShaw/PyPCAPKit/issues/575)). -- `httpv2._guess_version` tried to parse a stream as HTTP/2 and read failure as "not HTTP/2", rather than identifying the version first, so a genuine connection preface followed by a real `SETTINGS` frame raised `ProtocolError: unknown HTTP version`. It now identifies before it parses: a preface is recognised as HTTP/2 outright, skipped rather than fed to `httpv2`, and counted as header so `info` stays byte-identical to reading the following frame alone (otherwise the injected `packet=self.packet.payload`, sliced from octet 9 of a buffer whose frame starts at 24, reported preface remnants as payload). A preface with no frame behind it raises `ProtocolError: HTTP/2: connection preface with no frame`; a bare mid-stream `SETTINGS` frame, an `Upgrade: h2c` request and non-HTTP input are unchanged, the frame and the request left undecidable on purpose (`Upgrade: h2c` needs per-connection state a single payload cannot carry, and a mid-stream frame has no safe heuristic, since a `type <= 9` guess misfires on binary HTTP/1 bodies). Protochain over the sample captures is unchanged ([#800](https://github.com/JarryShaw/PyPCAPKit/issues/800)). +- `HTTP._guess_version()` tried to parse a stream as HTTP/2 and read failure as "not HTTP/2", rather than identifying the version first, so a genuine connection preface followed by a real `SETTINGS` frame raised `ProtocolError: unknown HTTP version`. It now identifies before it parses: a preface is recognised as HTTP/2 outright, skipped rather than fed to `httpv2`, and counted as header so `info` stays byte-identical to reading the following frame alone (otherwise the injected `packet=self.packet.payload`, sliced from octet 9 of a buffer whose frame starts at 24, reported preface remnants as payload). A preface with no frame behind it raises `ProtocolError: HTTP/2: connection preface with no frame`; a bare mid-stream `SETTINGS` frame, an `Upgrade: h2c` request and non-HTTP input are unchanged, the frame and the request left undecidable on purpose (`Upgrade: h2c` needs per-connection state a single payload cannot carry, and a mid-stream frame has no safe heuristic, since a `type <= 9` guess misfires on binary HTTP/1 bodies). Protochain over the sample captures is unchanged ([#800](https://github.com/JarryShaw/PyPCAPKit/issues/800)). - `TCP.__proto__` bound `httpv1.HTTP` directly for ports 80 and 8080, so a segment on either port was HTTP/1 by assertion of the port alone; both versions share those ports, so the payload has to decide. Repointed to the generic `pcapkit.protocols.application.http.HTTP` proxy, whose identification became positive only once [#800](https://github.com/JarryShaw/PyPCAPKit/issues/800) and [#814](https://github.com/JarryShaw/PyPCAPKit/pull/814) landed, which is why this waited. `udp.py` already bound the proxy for both ports; this removes the asymmetry its docstring and `docs/source/contributing/pep.rst` documented as an open request. Over the sample captures every HTTP/1.1 frame keeps its chain, and the genuine HTTP/2 frames in `options-transport.pcap` change `Ethernet:IPv4:TCP:Raw` to `Ethernet:IPv4:TCP:HTTP/2`. Not labelled breaking, but not free: `TCP:80/8080` traffic that is neither valid HTTP/1 nor preface-carrying is now exposed to the fall-through arm, where the direct `httpv1` binding left it `Raw` regardless; a caller depending on that is the one who would notice ([#682](https://github.com/JarryShaw/PyPCAPKit/issues/682)). - **a breaking change to** more bespoke registries: step 2 of [#860](https://github.com/JarryShaw/PyPCAPKit/issues/860), ahead of `AppType` ([#874](https://github.com/JarryShaw/PyPCAPKit/pull/874), above), bringing `StatusCode`, `ReturnCode`, `ResponseKind`, `GroupingInformation`, `OptionType`, `FEATCode`, `Command` and `Method` onto `EnumRegistry` and applying [#775](https://github.com/JarryShaw/PyPCAPKit/issues/775)'s mint/unmint ruling to their `get()` as well as `_missing_`. `StatusCode`, `ReturnCode`, `ResponseKind`, `GroupingInformation` and `OptionType` convert their unambiguous placeholder branches (`Unassigned`, `Unknown`, `opt_unknown`) to `_unregistered_member`; those with a custom `__new__` (`StatusCode`, `ReturnCode`, `OptionType`) get an override reconstructing the attributes the base's generic helper would leave unset, and `StatusCode`/`ReturnCode`'s hand-written `get()`, still on the retired `default == -1` convention, is replaced by the base's (no caller relied on the old form). `OptionType` keeps its own `get()` for its multi-namespace dispatch, and both its int/namespace and `str` paths build an unregistered member, where each minted one; the live pcapng parse path (`PCAPNG._make_pcapng_options()`) calls it with wire bytes, so parsing an undeclared option code registered a permanent member. `FEATCode`, `Command` and `Method` mint the literal wire value as its own name rather than any placeholder; per the owner's ruling that `get()` lacks the information to create new members (`Command` needs `feat`/`desc`/`type`/`conf` and `Method` needs `safe`/`idempotent`, which a bare wire string lacks), both `_missing_` and each class's own `get()` (an independent mint site of its own) now build an unregistered member. `FEATCode`'s crawler now declares every real FEAT-code value from the live IANA table instead of minting some of them as a side effect of evaluating `Command`'s rows at import time, the import-time-mutation shape [#861](https://github.com/JarryShaw/PyPCAPKit/pull/861) removed from `FilterType`; pinned count-agnostically so a future IANA update cannot fail a correct regeneration. Untouched, per rulings: `CommandType` stays `IntFlag` (real `A|P` composites in the generated data), and `TransportProtocol`'s `auto()` renumbering and `AppType` are [#874](https://github.com/JarryShaw/PyPCAPKit/pull/874)'s. `pcapkit/protocols/schema/misc/pcapng.py`'s `OptionEnumField.post_process` docstring is corrected: it still bypasses `OptionType.get()` directly, now for `pickle` round-trip safety ([#860](https://github.com/JarryShaw/PyPCAPKit/issues/860) already stops the minting that used to be the reason), since neither of `OptionType`'s paths gets both pickling and correct rendering right at once ([#869](https://github.com/JarryShaw/PyPCAPKit/pull/869)). - `register` on `ProtocolBase`, `Frame`, `PCAPNG`, `SCTP`, `Link`, `Internet` and the concrete `Transport` subclasses (`TCP`, `UDP`) now raises `RegistryError` for a non-class argument, where it raised a bare `TypeError` ("issubclass() arg 1 must be a class") from inside `abc`. Each guard gains an explicit `isinstance(protocol, type)` test ahead of its `issubclass`, after the `ModuleDescriptor` is unwrapped, so a descriptor naming a non-class attribute is rejected too. Every such `Raises:` clause already promised `RegistryError` if `protocol` "is not a ... subclass", which a non-class satisfied in spirit but not in fact; they now read "is not a class, or not a ... subclass". No guard's target class changes, and a wrong class still raises `RegistryError` as before. `Transport.register()` on `Transport` itself still raises `UnsupportedCall` first; only its subclasses reach the guard. Not breaking for exception handling: `RegistryError` subclasses `TypeError` through `BaseError`, so `except TypeError` still catches it, and the type and message text are the only things a handler can key on that change. Observable side effects do come with it, because `BaseError` is loud by default: the non-class path now emits pcapkit's one `CRITICAL` log record, and outside devmode it installs `sys.excepthook` and `threading.excepthook`, neither of which the bare `TypeError` from `abc` did. That is the wrong-class path's existing behaviour, so the fix makes the two consistent rather than introducing something new. Follows the same fix for the foundation registrars ([#1021](https://github.com/JarryShaw/PyPCAPKit/issues/1021)) ([#1026](https://github.com/JarryShaw/PyPCAPKit/issues/1026)). @@ -340,7 +340,8 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r - a declared length above `sys.maxsize`, such as a PCAP `incl_len` of `2**31 - 16` on GraalPy or 32-bit CPython, raised `OverflowError` from a read or seek. Every length-driven read in `Schema.unpack()`, `Frame.read()` and `PCAPNG.read()` is bounded at `sys.maxsize`, so such a claim gets the overrun handling it gets on 64-bit CPython ([#1975](https://github.com/JarryShaw/PyPCAPKit/issues/1975), [#1979](https://github.com/JarryShaw/PyPCAPKit/pull/1979)). - PCAP-NG blocks cut short by the file or by their own framing no longer abort an extraction. A Decryption Secrets Block whose `secrets_length` runs past the block keeps the declared length and warns, its secrets capped at what the block holds ([#1980](https://github.com/JarryShaw/PyPCAPKit/issues/1980), [#1983](https://github.com/JarryShaw/PyPCAPKit/pull/1983)); an unknown, Custom or systemd journal export block the file truncates gets the "block truncated" warning the other block types give ([#1984](https://github.com/JarryShaw/PyPCAPKit/issues/1984), [#1988](https://github.com/JarryShaw/PyPCAPKit/pull/1988)); and a malformed TLS or WireGuard key-log line warns with `ProtocolWarning` and is left out of `entries`, where it raised `FieldValueError` ([#1985](https://github.com/JarryShaw/PyPCAPKit/issues/1985), [#1989](https://github.com/JarryShaw/PyPCAPKit/pull/1989)). A ZigBee NWK or APS key off its fixed width rebuilds as captured, where it was zero-padded or cut to that width ([#1986](https://github.com/JarryShaw/PyPCAPKit/issues/1986), [#2001](https://github.com/JarryShaw/PyPCAPKit/pull/2001)). - capture timestamps convert without the platform's `fromtimestamp` limits. Windows' C runtime covers only 1970 to 3000, so a PCAP-NG timestamp outside that range fell back to 1970 with `invalid timestamp`, and the Mobility Header's NTP and PMIPv6 timestamps had the same gap. PCAP, PCAP-NG and MH convert through `pcapkit.utilities.datetime.fromtimestamp()`, which gives the stdlib's result on Linux ([#2099](https://github.com/JarryShaw/PyPCAPKit/issues/2099), [#2111](https://github.com/JarryShaw/PyPCAPKit/pull/2111)), and encode through `pcapkit.utilities.datetime.totimestamp()`, since GraalPy's `timestamp()` put a pre-1970 instant with a fraction one second late ([#2134](https://github.com/JarryShaw/PyPCAPKit/issues/2134), [#2136](https://github.com/JarryShaw/PyPCAPKit/pull/2136)). -- ARP's address makers refuse what they cannot pack: a `bool` protocol address raises `FieldValueError` whatever `ptype` is; a hardware address that is none of `str`, `bytes` and `bytearray` (a `bool` included), or a protocol address not packed as IPv4 or IPv6 that is none of those and not an `ipaddress` address, raises `ProtocolError`, where both failed later with a bare `TypeError`. A `bytearray` packs as `bytes` ([#2166](https://github.com/JarryShaw/PyPCAPKit/issues/2166), [#2182](https://github.com/JarryShaw/PyPCAPKit/pull/2182), [#2256](https://github.com/JarryShaw/PyPCAPKit/issues/2256)). +- ARP's address makers refuse what they cannot pack: a `bool` protocol address raises `FieldValueError` whatever `ptype` is; a hardware address that is none of `str`, `bytes` and `bytearray` (a `bool` included), or a protocol address not packed as IPv4 or IPv6 that is none of those and not an `ipaddress` address, raises `ProtocolError`, where both failed later with a bare `TypeError`. A `bytearray` packs as `bytes` ([#2166](https://github.com/JarryShaw/PyPCAPKit/issues/2166), [#2182](https://github.com/JarryShaw/PyPCAPKit/pull/2182), [#2256](https://github.com/JarryShaw/PyPCAPKit/issues/2256), [#2257](https://github.com/JarryShaw/PyPCAPKit/pull/2257)). +- Ethernet's and PCAP-NG's MAC address makers, and PCAP-NG's EUI address maker, raise `ProtocolError` for an address that is none of `str`, `bytes` and `bytearray` (a `bool` included), where they raised a bare `TypeError` ([#2222](https://github.com/JarryShaw/PyPCAPKit/issues/2222), [#2232](https://github.com/JarryShaw/PyPCAPKit/pull/2232)). - the Quick-Start rate parses as the `int` the IPv4, HOPOPT, IPv6-Opts and TCP data models declare, where it was a `float` such as `80.0`, and a TCP Fast Open cookie request, the option of length 2, parses with `cookie` as `b''` rather than `None` ([#2167](https://github.com/JarryShaw/PyPCAPKit/issues/2167), [#2185](https://github.com/JarryShaw/PyPCAPKit/pull/2185)). - PCAP-NG's `ns_dnsIP6addr` option and IPv6 name resolution record build with their defaults, `2001:4860:4860::8888` and `::1`, where IPv4 defaults made them raise `FieldValueError` ([#2168](https://github.com/JarryShaw/PyPCAPKit/issues/2168), [#2179](https://github.com/JarryShaw/PyPCAPKit/pull/2179)). - message text: the `epb_flags` and `pack_flags` length errors say `expected 4`, the length they check, rather than `expected 8` ([#2168](https://github.com/JarryShaw/PyPCAPKit/issues/2168), [#2179](https://github.com/JarryShaw/PyPCAPKit/pull/2179)), and the IPv4 TS option's unknown-flag warning and the Mobility Header's prefix error spell `timestamp` and `mobile` ([#2171](https://github.com/JarryShaw/PyPCAPKit/issues/2171), [#2189](https://github.com/JarryShaw/PyPCAPKit/pull/2189)). @@ -372,7 +373,7 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r - `pcapkit.utilities.logging` as a real interface: `get_logger()` for per-module children, `configure()` to set level, handler, stream, format or propagation at runtime, `reset()` to return to library-neutral, and `ensure_output()`. Each module that logs now does so under its own `__name__`, so a consumer can silence `pcapkit.foundation.registry` while keeping `pcapkit.foundation.extraction` ([#384](https://github.com/JarryShaw/PyPCAPKit/pull/384)). - `pcapkit.utilities.config`, one module for every tunable limit, each read once from a `PCAPKIT_*` environment variable: the frame-layer and extension-header limits, the pypcapfile engine's TCP-over-IPv6 warning limit, the dumpkit fallback, the charset-detection cache, the read chunk and the zero-padding guards. A bad value warns with `ConfigWarning` and falls back to the default; `-1` lifts a limit. The homes these limits had in the prereleases keep no re-export ([#1726](https://github.com/JarryShaw/PyPCAPKit/issues/1726), [#1743](https://github.com/JarryShaw/PyPCAPKit/pull/1743)). -- `pcapkit.utilities.datetime`, holding `fromtimestamp()` and `totimestamp()`, the timestamp conversions of the capture-timestamp entry under `pcapkit.protocols` ([#2111](https://github.com/JarryShaw/PyPCAPKit/pull/2111), [#2136](https://github.com/JarryShaw/PyPCAPKit/pull/2136)). Its first name, `pcapkit.utilities.timestamp`, shipped in no release. +- `pcapkit.utilities.datetime`, holding `fromtimestamp()` and `totimestamp()`, the timestamp conversions of the capture-timestamp entry under `pcapkit.protocols` ([#2111](https://github.com/JarryShaw/PyPCAPKit/pull/2111), [#2136](https://github.com/JarryShaw/PyPCAPKit/pull/2136)). #### Changed @@ -397,7 +398,7 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r - `re.sub` sites under `pcapkit/vendor/` passed `re.MULTILINE` as the fourth *positional* argument, which is `count`, not `flags`: substitution capped at 8 matches, and a `DeprecationWarning` on Python 3.13+ (a future `TypeError`). Found by an AST sweep rather than grep, since most put the flag on a continuation line. The flag moves to `flags=` at every site, in `default.py`, `hip/eddsa_curve.py`, `http/frame.py`, `http/method.py`, `http/status_code.py`, `ipv4/router_alert.py`, `ipv6/option.py`, `ipv6/router_alert.py`, `ipv6/tagger_id.py`, `reg/ethertype.py`, `tcp/flags.py`, `tcp/mp_tcp_option.py` and `tcp/option.py`. Behaviour preserving: the shared pattern (`r'\r*\n'`) has no MULTILINE-sensitive anchor, so no `const/` regeneration was needed ([#796](https://github.com/JarryShaw/PyPCAPKit/issues/796)). - `main` was red ([#866](https://github.com/JarryShaw/PyPCAPKit/issues/866)) with `RecursionError` in `tests/const/test_const_enum_lookup.py`, `tests/const/test_const_enum_no_mint.py` and `tests/protocols/misc/test_pcapng_unit.py`. `pcapkit/vendor/pcapng/record_type.py` and `secrets_type.py` rendered a `_missing_` body whose first line did not return: `cls._unregistered_member(value, 'Unassigned')` followed by `return cls(value)`. That was harmless while the first line was `extend_enum(...)`, which registers the member so `cls(value)` found it; [#861](https://github.com/JarryShaw/PyPCAPKit/pull/861) (above) replaced it with `_unregistered_member`, which deliberately does not register, so `cls(value)` missed and re-entered `_missing_`. [#861](https://github.com/JarryShaw/PyPCAPKit/pull/861) fixed the *generated* modules but not these *crawlers*, so a live regeneration reintroduced the defect. Both crawlers now collapse to the single returning line the other registries use, and their const files are regenerated. A new guard at the crawler-emission layer requires a hard-coded `_missing_` body to end in a return and no emitted line to re-enter `_missing_` for the same value -- deliberately narrower than "every emitted line must return", which `Vendor.process()`'s shape legitimately breaks. The guard sees only some crawlers; the rest are covered behaviourally. Closes [#866](https://github.com/JarryShaw/PyPCAPKit/issues/866) ([#867](https://github.com/JarryShaw/PyPCAPKit/pull/867)). - `python -m pcapkit.vendor ` could not fail: `run()` swallowed every crawler exception as a filterable `VendorRuntimeWarning` and `main()` always returned `0`, so a broken crawler regenerating nothing looked like success to a human and to the cron job. Per the owner's ruling -- discard only what a failing sub-vendor changed, and let a cron job warn about failed sub-vendors without failing as a whole -- the pieces below land. **Exit code** (`pcapkit/vendor/__main__.py`): `run()` returns whether its target succeeded; `main()` attempts every target and returns `1` if any raised, writing the failing target's qualified name and exception `repr` to stderr unconditionally via `print()` rather than the filterable warning. **Per-target snapshot and restore**: a new `_snapshot_and_restore` context manager backs up a target's const file with `tempfile.mkstemp` before it runs (resolving the destination through `Vendor._dest_path()`, now a `classmethod` in `pcapkit/vendor/default.py` so it can be called before `Vendor.__init__` fetches, renders or writes), restores the backup via `os.replace` on any exception before re-raising, and discards it on success. A target whose destination cannot be resolved (a crawler outside the real tree) runs unprotected rather than failing the snapshot step, since `Vendor.__init__` hits the same resolution failure itself; a destination that does not exist yet is left alone. **Workflow** (`.github/workflows/cron-vendor.yml`): the "Update Vendor" step no longer aborts under `bash -e` when `pcapkit-vendor` exits non-zero. The exit code is captured around that one command, so targets that succeeded are still sorted by `isort`, diffed, committed and pushed, while a "Warning" block naming the failed target(s) is appended to `$GITHUB_STEP_SUMMARY`, a `::warning::` annotation is emitted per failure, and the step's own exit code is always `0`. A per-target `git checkout -- ` restore step was dropped: with snapshot/restore, a failed target's const file is never touched. Closes [#872](https://github.com/JarryShaw/PyPCAPKit/issues/872) ([#873](https://github.com/JarryShaw/PyPCAPKit/pull/873)). -- `_snapshot_and_restore` ([#873](https://github.com/JarryShaw/PyPCAPKit/pull/873)) carried an unpinned invariant and inaccurate prose and lint, none of it changing behaviour. **1.** `except BaseException:` in `_snapshot_and_restore` is pinned: a test asserts that a `KeyboardInterrupt` still restores the file and propagates. **2.** A false docstring clause is corrected: a crawler whose `_dest_path` is an instance-method-only override fails the *class* call `_snapshot_and_restore` makes but not the *instance* call inside `__init__`, so that target runs unprotected and **succeeds silently**. **3.** The `# pylint: disable=no-else-raise` pragma is **kept**: the belief that R1720 never applies to `try`/`except`/`else` was a message-control artefact (`--disable=all --enable=no-else-raise` does not re-enable the check the way `--enable=R` does), and under the `Makefile`'s flags, which CI runs, pylint flags the `else:`, which is deliberate because it makes the backup cleanup unreachable from the `except`. Left out of scope: an untested `os.close(fd)` leak (harmless, since `copy2` reopens the path) and the semantically identical unindented form of the same `else:`. Closes [#875](https://github.com/JarryShaw/PyPCAPKit/issues/875) ([#876](https://github.com/JarryShaw/PyPCAPKit/pull/876)). +- `_snapshot_and_restore` ([#873](https://github.com/JarryShaw/PyPCAPKit/pull/873)) carried an unpinned invariant and inaccurate prose and lint, none of it changing behaviour. **1.** `except BaseException:` in `_snapshot_and_restore` is pinned: a test asserts that a `KeyboardInterrupt` still restores the file and propagates. **2.** A false docstring clause is corrected: a crawler whose `_dest_path` is an instance-method-only override fails the *class* call `_snapshot_and_restore` makes but not the *instance* call inside `__init__`, so that target runs unprotected and **succeeds silently**. **3.** The `# pylint: disable=no-else-raise` pragma is **kept**, because it is needed: under the `Makefile`'s flags, which CI runs, pylint reports R1720 on the `else:`. A `--disable=all --enable=no-else-raise` run reports nothing there, since enabling the one message does not re-enable the check on a `try`/`except`/`else` the way `--enable=R` does, so such a run cannot show the pragma unneeded. The `else:` is deliberate: it makes the backup cleanup unreachable from the `except`. Left out of scope: an untested `os.close(fd)` leak (harmless, since `copy2` reopens the path) and the semantically identical unindented form of the same `else:`. Closes [#875](https://github.com/JarryShaw/PyPCAPKit/issues/875) ([#876](https://github.com/JarryShaw/PyPCAPKit/pull/876)). - `TLSKeyLabel` tracked the Mozilla NSS key-log wiki, but `draft-ietf-opsawg-pcapng-06` section 4.7 now points at `[I-D.ietf-tls-keylogfile]` (draft-05), published as [RFC 9850](https://datatracker.ietf.org/doc/html/rfc9850), "The SSLKEYLOGFILE Format for TLS", whose section 4.2 creates the IANA "TLS SSLKEYLOGFILE Labels" registry. Against it, `TLSKeyLabel` was missing `ECH_SECRET` and `ECH_CONFIG` ([RFC 9850](https://datatracker.ietf.org/doc/html/rfc9850) sections 2.3 and 4.2), so `TLSKeyLabel('ECH_SECRET')` raised `ValueError` on a legitimate Decryption Secrets Block; both are added, `ECH_SECRET` keeping the file's `# nosec B105` bandit convention for `*_SECRET` members. `RSA` has no row in [RFC 9850](https://datatracker.ietf.org/doc/html/rfc9850)'s registry (it is an NSS-only label the NSS wiki records as removed in NSS 3.34) and is kept, since removing a public member is breaking, with a docstring comment marking it the historical exception. `pcapkit/vendor/pcapng/secrets_type.py`'s stale `# NSS Key Log Format` comment is repointed at RFC 9850, and `TLSKeyLabel`'s docstring cites it. A new test pins the member set against RFC 9850 plus the `RSA` exception. Closes [#882](https://github.com/JarryShaw/PyPCAPKit/issues/882) ([#883](https://github.com/JarryShaw/PyPCAPKit/pull/883)). - both spellings of an IANA citation render as `:rfc:` roles in the generated `pcapkit.const` docstrings ([#1664](https://github.com/JarryShaw/PyPCAPKit/issues/1664), [#1673](https://github.com/JarryShaw/PyPCAPKit/pull/1673)). - `MAX_RETRY` and `CI_MODE` stay in `pcapkit.vendor.default`, read from `PCAPKIT_VENDOR_RETRY` and `PCAPKIT_CI_MODE`; a bad value warns with `ConfigWarning` and falls back, where 1.4.x crashed on a non-integer and read `0` as `1` ([#1745](https://github.com/JarryShaw/PyPCAPKit/pull/1745)). @@ -437,6 +438,7 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r - `engine='pyshark'` had no CI job driving it through a real `tshark`-parsed capture. `pypcap-parity` installs `pyshark` but its `apt-get install` named no `tshark` package, so `PyShark.unsupported_reason()` declined the engine, the extractor fell back to the built-in parser, and the only real-extraction assertion, `assertGreater(extractor.length, 0)`, passed against the fallback's output: a vacuous pass invisible in every skip count. `pypcap-parity` now installs `tshark` with the debconf pre-seed `engine-tests` carries, and the test proves which engine ran (no `EngineWarning`, `extractor._exnam == 'pyshark'`, a `PyShark` instance), as the PyPCAP parity test does. Only `tests/integration/test_engine_runtime.py` and `test_engine_parity.py` change; the length assertion is kept ([#846](https://github.com/JarryShaw/PyPCAPKit/pull/846)). - `engine-tests` ran a Python-version leg per interpreter regardless of which engines that interpreter could install, so `pyproject.toml`'s `PyPCAPFile = ["pypcapfile; python_version < '3.12'"]` marker resolved to *nothing* on 3.12--3.14, yet the `Engines Python 3.12/3.13/3.14` legs passed and, once required, gated merges while exercising an engine that was never installed. Rebuilt as a Python x engine matrix in which every cell has an expected outcome: `supported` cells install through the real extra and must pass; `unsupported` cells (`PyPCAPFile` on 3.12--3.15, `PyShark` on 3.14/3.15) install by raw `pip install` to bypass the marker and assert the engine's own `PYTHON_CEILING` decline; and `not-installable` cells (`PyPCAP` on 3.12--3.15) log a genuine C-extension build failure and skip the test step. The install step never passes silently in either direction. Python 3.15 is `continue-on-error` throughout. Job names change shape (`Engines Python X` becomes `Engines Python X (engine)`), so the branch-protection ruleset's required-check list was updated in the same window, lest a required check that stopped existing block every merge ([#849](https://github.com/JarryShaw/PyPCAPKit/pull/849)). - Ruleset `23497679`'s `required_status_checks` names exact check contexts, and GitHub rulesets match literally with no wildcard, so the list needs hand-editing on every matrix change. The old `Engines Python ` names in it are dead since [#849](https://github.com/JarryShaw/PyPCAPKit/pull/849); the `Compat Python 3.10`-`3.14` names stay live, emitted by `python-compatibility.yml`, and are untouched. Adds a job, `required-checks` (context `Required checks passed`), depending on `[test, integration, engine-tests, pypcap-parity]` with `if: always() && inputs.gate-only != true` and an explicit per-dependency check that fails loudly, naming the job, on anything but `success`: a bare `needs:` list would leave it *skipped* rather than failed when a dependency failed, and GitHub's docs list a skipped required check as passing. That context is meant to replace the ruleset's `test`, `integration`, `Engines` and `pypcap-parity` entries once the ruleset is updated separately, as a repository setting outside the change; the `Compat` names are excluded from the replacement. Only `.github/workflows/unit-tests.yml` changes; no library or test code ([#856](https://github.com/JarryShaw/PyPCAPKit/pull/856)). +- `engine-tests` runs one job per engine, `Engines ()`, each looping over its interpreters in fresh virtual environments, in place of [#849](https://github.com/JarryShaw/PyPCAPKit/pull/849)'s job per Python and engine. `required-checks` also needs `changes` and `project-tests`: `project-tests` must pass on every diff, and a skipped leg passes only where `changes` classifies the diff as docs-only ([#1052](https://github.com/JarryShaw/PyPCAPKit/issues/1052), [#1055](https://github.com/JarryShaw/PyPCAPKit/pull/1055)), which may skip `test`, `integration`, `engine-tests` and `pypcap-parity`, or as test-only, which may skip `engine-tests` and `pypcap-parity` ([#2243](https://github.com/JarryShaw/PyPCAPKit/issues/2243), [#2244](https://github.com/JarryShaw/PyPCAPKit/pull/2244)). - the prose the post-[#719](https://github.com/JarryShaw/PyPCAPKit/issues/719) defect wave added or touched, checked against the code; no code changes. `MesgIDOption.seconds` in the Mobility Header schema said seconds since 1970 where it holds an [RFC 1305](https://datatracker.ietf.org/doc/html/rfc1305) NTP count from 1900; `HTTP._guess_version()` comments predated the HTTP/2 gate; and the `_parse_next_layer` note miscounted the captured octets. `Extractor._open_output()` now says only the PCAP engine passes header fields, `Extractor.run()` and the PCAP-NG engine's `run` list the `FormatError` for PCAP output from a PCAP-NG input, and the extraction and engine pages document the PCAP-output path ([#1179](https://github.com/JarryShaw/PyPCAPKit/pull/1179), [#1180](https://github.com/JarryShaw/PyPCAPKit/pull/1180), [#1186](https://github.com/JarryShaw/PyPCAPKit/pull/1186), [#1103](https://github.com/JarryShaw/PyPCAPKit/issues/1103)). - **a breaking change to** the `all` extra: it carries the core addons only -- `cli`, `crypto` and `NGAP`. The third-party engines 1.4.x bundled in it (`dpkt`, `scapy` and `pyshark`) and the vendor crawler's `requests` and `beautifulsoup4` are no longer in it; the engines keep their own `DPKT`, `Scapy` and `PyShark` extras and the crawler its `vendor` extra, and `pypcapfile` has a new `PyPCAPFile` extra of its own. A new `dev` extra carries what CI needs ([#910](https://github.com/JarryShaw/PyPCAPKit/issues/910), [#914](https://github.com/JarryShaw/PyPCAPKit/pull/914)). - the `Scapy` and `dev` extras require `scapy>=2.6.0` ([#1572](https://github.com/JarryShaw/PyPCAPKit/issues/1572), [#1578](https://github.com/JarryShaw/PyPCAPKit/pull/1578)), and the `docs` extra `Sphinx>=7.3`, the first release with `versionremoved` ([#1743](https://github.com/JarryShaw/PyPCAPKit/pull/1743)), and an exact `sphinx-autodoc-typehints` pin ([#1856](https://github.com/JarryShaw/PyPCAPKit/issues/1856), [#1877](https://github.com/JarryShaw/PyPCAPKit/pull/1877)), 3.17.2 since the weekly dependency update ([#1944](https://github.com/JarryShaw/PyPCAPKit/pull/1944)). The `test` extra gains `psutil`, `markdown-it-py` and `lxml`, which the scripts under `util/` use ([#1894](https://github.com/JarryShaw/PyPCAPKit/issues/1894), [#1896](https://github.com/JarryShaw/PyPCAPKit/pull/1896)). The `crypto` and `all` extras cap `cryptography` below 46 on PyPy older than 3.11, for which no later release has a Linux wheel ([#2040](https://github.com/JarryShaw/PyPCAPKit/issues/2040), [#2046](https://github.com/JarryShaw/PyPCAPKit/pull/2046)). @@ -452,7 +454,7 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r - `examples/generators/options.py`'s `TCP_BASE` spelled header fields with names `TCP.make()` does not declare, so every option fixture in `examples/captures/options-tcp.pcap` was built from values the generator had not asked for. `make` ends in `**kwargs` and reads nothing out of it, so an undeclared keyword is accepted and discarded with no warning. `'seq': 1` was `seq_no`, so every captured frame carried sequence number 0; `'urgent_pointer'` was `urgent` (requested value and default both 0); and `'ack_flag': False` was `ack` while `'ack': 0` bound to `ack` itself, the acknowledgement *flag* rather than the number (`ack_no`), so that pair was wrong both ways. Each is renamed to the parameter `make` declares, keeping the intended values. `TCP(**TCP_BASE).info.seq` was 0 against a declared 1 and is now 1, and `options-tcp.pcap` changes only in the low octet of each frame's sequence number, with no case status or `EXPECTED_FAILURES` entry moving. The module's other base mappings were audited: the rest pass nothing their `make` does not declare, and HIP's `extension` is declared by `HIP.read()`, this library's documented way of forwarding a read-path keyword, not another instance ([#602](https://github.com/JarryShaw/PyPCAPKit/issues/602)). - `util/bump_version.py` left `CITATION.cff` naming the previous release. Nothing else maintains that file, so every bump since [#615](https://github.com/JarryShaw/PyPCAPKit/pull/615) would have stranded the `version` and `date-released` that GitHub's "Cite this repository" button, citation managers, Zenodo and dependency inventories read. Both fields now move with `__version__`, together, since the file's header says both describe the newest *published* release and moving only one would assert that 1.5.0b5 was released on the day 1.5.0b4 was. The date is UTC, because seven of the thirty most recent bumps were made late evening US-Eastern, where a local date is a day behind the publish; the bump triggers `create-release.yml`, the median gap to the PyPI upload is three minutes, and the UTC dates agree 30 times of 30. The rewrite is line-oriented, so the comment header, key ordering and each field's quoting survive (`cff-version` and a `references` entry's own `version` are anchored out at column zero), and the result is checked with `cffconvert --validate`. An absent file is reported on stderr and skipped rather than failing the vendor cron before its `git commit`, which would discard a whole registry crawl for a documentation file; a file with no `version` field raises before anything is written, since reporting success while rewriting nothing is the staleness being fixed. The script gains a `main()` guard (it ran the whole bump at import, hence no testable surface), and the `import pcapkit` fallback in its version reader, which returned `"1.5.0b4'\n"` (closing quote and newline, which `packaging` rejects), is fixed; it never worked and went unnoticed because the only caller installs the package first. A new gate asserts the committed file still names the packaged version, covering hand-made version changes that never run this script ([#625](https://github.com/JarryShaw/PyPCAPKit/pull/625)). - `LICENSE`'s copyright notice began the term at 2018, a year after the work it covers. The repository's first commit is `c57f7d0b7`, dated 2017-11-07, so the notice understated the term and contradicted `docs/source/conf.py`, whose Sphinx footer is `f'2017-{datetime.date.today().year}, Jarry Shaw'`. The wrong start year survived every maintenance pass because each looked only at the other end of the range: `bc836cfa2` (2020-05-31) introduced `2018-2020` when the BSD-3-Clause text replaced MPL 2.0, and hand bumps to `2018-2022`, `2018-2023` and `2018-2026` ([#615](https://github.com/JarryShaw/PyPCAPKit/pull/615)) each copied `2018` forward. 2017 is **restored** rather than newly asserted: the initial MIT `LICENSE` read `Copyright (c) 2017 Jarry Shaw`, and the GPL v3 era carried `Copyright (C) 2017 Jarry Shaw` in its appendix; it was lost on 2018-12-08, when `4f3e00d5f` relicensed to Apache 2.0 under `Copyright 2018 Jarry Shaw` and `1c69341dc` replaced that with MPL 2.0 text with no author notice. The end year is **dropped** rather than automated: the notice ships inside the sdist and wheel, so its text is fixed at build time in every copy already downloaded and cannot be computed like the docs footer; copyright subsists from creation whether or not a notice names the current year; and BSD-3-Clause's canonical form is a singular `Copyright (c) `. A range was six years of hand maintenance buying nothing, and automation was considered and rejected, since automating a value that need not be current is worse than not carrying it. `conf.py` is untouched (a self-maintaining footer showing a range is conventional and correct). The `BSD 3-Clause License` title line and the comma in the notice follow choosealicense.com's template, whose first line is that title and whose notice reads `Copyright (c) [year], [fullname]`. The licence body is unaltered: the change is confined to the copyright line, and the `DAMAGE.` that [#615](https://github.com/JarryShaw/PyPCAPKit/pull/615) repaired from `DAMAGE.s` is still clean ([#630](https://github.com/JarryShaw/PyPCAPKit/pull/630)). -- `CITATION.cff` shipped in no source distribution. `MANIFEST.in` has an `include` each for `README.md`, `LICENSE` and `CHANGELOG.md` but none for the citation file, and its `global-include` patterns (`*.rst`, `*.py`) cannot match a `.cff`. Removing those `include` lines and rebuilding shows `README.md` and `LICENSE` shipping anyway (setuptools adds the latter from `license_files`, recorded as `License-File: LICENSE` in `PKG-INFO`) while `CHANGELOG.md` vanishes, so only `CHANGELOG.md` is load-bearing, and a citation file, which no packaging default covers, is in the same position. The gap is invisible from the web UI, because GitHub renders "Cite this repository" from the repository; citation managers, Zenodo and dependency inventories read the published artifact, the surface that was missing it. [#615](https://github.com/JarryShaw/PyPCAPKit/pull/615) flagged the omission and [#619](https://github.com/JarryShaw/PyPCAPKit/pull/619) did not address it. It matters because [#625](https://github.com/JarryShaw/PyPCAPKit/pull/625) taught `util/bump_version.py` to keep the file's `version` and `date-released` in step, and a release exercising that path would publish an sdist omitting the artefact under test. It also lets `RepositoryCitationTests` in `tests/project/test_bump_version.py` (the gate [#625](https://github.com/JarryShaw/PyPCAPKit/pull/625) added for hand-authored bumps) run against an unpacked sdist, where it skipped itself with "CITATION.cff is not shipped in the source distribution". `python -m build --sdist` now differs from before only by the archive entry `pypcapkit-1.5.0b4/CITATION.cff`, the shipped copy is byte-identical, and `twine check --strict` passes on both ([#631](https://github.com/JarryShaw/PyPCAPKit/pull/631)). +- `CITATION.cff` shipped in no source distribution. `MANIFEST.in` had an `include` each for `README.md`, `LICENSE` and `CHANGELOG.md` but none for the citation file, and its `global-include` patterns (`*.rst`, `*.py`) could not match a `.cff`. Removing those `include` lines and rebuilding shows `README.md` and `LICENSE` shipping anyway (setuptools adds the latter from `license_files`, recorded as `License-File: LICENSE` in `PKG-INFO`) while `CHANGELOG.md` vanishes, so only `CHANGELOG.md` is load-bearing, and a citation file, which no packaging default covers, is in the same position. The gap is invisible from the web UI, because GitHub renders "Cite this repository" from the repository; citation managers, Zenodo and dependency inventories read the published artifact, the surface that was missing it. [#615](https://github.com/JarryShaw/PyPCAPKit/pull/615) flagged the omission and [#619](https://github.com/JarryShaw/PyPCAPKit/pull/619) did not address it. It matters because [#625](https://github.com/JarryShaw/PyPCAPKit/pull/625) taught `util/bump_version.py` to keep the file's `version` and `date-released` in step, and a release exercising that path would publish an sdist omitting the artefact under test. It also lets `RepositoryCitationTests` in `tests/project/test_bump_version.py` (the gate [#625](https://github.com/JarryShaw/PyPCAPKit/pull/625) added for hand-authored bumps) run against an unpacked sdist, where it skipped itself with "CITATION.cff is not shipped in the source distribution". `python -m build --sdist` now differs from before only by the archive entry `pypcapkit-1.5.0b4/CITATION.cff`, the shipped copy is byte-identical, and `twine check --strict` passes on both ([#631](https://github.com/JarryShaw/PyPCAPKit/pull/631)). - the release workflow published to PyPI and to Anaconda with nothing a human had to approve. `create-release.yml`'s `pypi` job had `environment: release` commented out while the `id-token: write` from the same upstream snippet was re-added live below it, so the block read as disabled as a unit when only its gate still was; the `conda` job had no `environment:` at all. Neither needed a tag: the workflow also fires on completion of `Vendor Update`, so a scheduled registry crawl that bumped the version was enough to publish to both public indexes, a median three minutes from bump to upload. Every job that reaches outside the run is now gated: `github` on `github-release` (the GitHub Release and the `v*` tag), `tag` on `conda-tag` (the commit pushed to `main`), `pypi` on `pypi`, `conda` on `anaconda`. One environment per credential rather than a shared `release`, because approval is granted to an environment and not a job, so one `release` approved once would release both indexes together. PyPI has to be answerable alone: an accepted version number cannot be reused, and `skip-existing: true` makes the re-upload *succeed* having published nothing, so an unintended publish permanently consumes the number the intended release wanted. The workflow half is only half the fix: an `environment:` naming one that does not exist is created implicitly with no protection rules and the job proceeds unapproved, so this is inert until each of these environments exists in repository settings with a required reviewer, and each one's "Deployment branches and tags" must stay unrestricted or the `tags: v*` trigger fails outright instead of pausing. `github-pages` is the standing example, carrying no protection rule since 2021. The new test asserts the general rule, that no job running a publishing action or a `git push` may omit an `environment:`, so a publishing job added later without a gate fails rather than ships ([#641](https://github.com/JarryShaw/PyPCAPKit/issues/641)). - test comments in `tests/_tiers.py`, `tests/test_tier_guard.py` and `tests/integration/_helpers.py` stated an exact count of committed captures under `examples/captures/`, a number that drifts whenever the tracked set changes, and `_helpers.py`'s undercounted on the day it was written. Each now describes the invariant (`_tiers.py`: "the moment somebody commits another capture... or stops committing one"; `_helpers.py`: "...are fixtures -- some of them committed --..."; `test_tier_guard.py` drops its ordinal). Prose-only, verified with `coverage.parser.PythonParser` that no executable statement moved ([#700](https://github.com/JarryShaw/PyPCAPKit/issues/700)). - `test_every_tracked_name_exists_and_matches_git` in `tests/test_tier_guard.py` checked neither the git index nor the filesystem despite its name: it asserted only that the tracked-capture list was non-empty and no name contained a `/`, so a hardcoded list passed whether or not it matched reality. Sibling `test_capture_suggestions_are_captures` had the same gap. Fixed in [#703](https://github.com/JarryShaw/PyPCAPKit/pull/703); [#703](https://github.com/JarryShaw/PyPCAPKit/pull/703)'s description names both [#700](https://github.com/JarryShaw/PyPCAPKit/issues/700) and [#708](https://github.com/JarryShaw/PyPCAPKit/issues/708). The first test now re-derives the expected set from `git ls-files -z` under `examples/captures/`, asserts equality against it, and checks each name with `Path.is_file()`; its sibling re-implements `committed_capture_names()`'s filter inline, which catches a wrong filter but still trusts `committed_captures()` for the tracked set. A hardcoded literal that happens to match the tracked names still passes; the test catches a set that is wrong when it runs ([#708](https://github.com/JarryShaw/PyPCAPKit/issues/708)). diff --git a/docs/source/changelog/1.5.0.rst b/docs/source/changelog/1.5.0.rst index 7af7dfc589..569314c54c 100644 --- a/docs/source/changelog/1.5.0.rst +++ b/docs/source/changelog/1.5.0.rst @@ -5,7 +5,7 @@ The largest release since 1.0, and the first recorded here as it happened rather than reconstructed. New extraction engines, ESP with payload decryption, SCTP and NGAP over SCTP, a library logger that no longer hijacks the consumer's, and a defect programme run through the issue tracker, citing issues and pull -requests from :pr:`340` to :pr:`2205`. +requests from :pr:`340` to :pr:`2257`. Preceded by ``1.5.0a1`` (2026-09-15), ``1.5.0b1`` to ``1.5.0b3`` (all 2026-09-19) and ``1.5.0b4`` to ``1.5.0b14`` (2026-09-20 to 2026-10-22), dated in UTC and all @@ -58,8 +58,7 @@ Changed output is unchanged (:issue:`804`). * ``test_const_enum_no_mint.py``'s ``is_manufactured()`` exemption for the hex-suffixed names :pr:`878` (below) preserves moves from a per-*file* to a - per-*name-shape* one. The per-file ``HEX_SUFFIXED_NAME_EXEMPT_PATHS`` was - shape-blind: rewriting a branch to ``'Xyplex_%d' % value`` -- exactly the + per-*name-shape* one. The per-file exemption was shape-blind: rewriting a branch to ``'Xyplex_%d' % value`` -- exactly the value-derived shape the predicate exists to flag -- left the sweep green. The check now exempts only a ``'..._0x%s' % hex(value)[2:].upper().zfill(4)`` expression, and the exact ``exempt_hits == 53`` bound stays. Test-only. Closes :issue:`879` @@ -1483,10 +1482,12 @@ Changed * **a breaking change to** ``PCAPNG.context``: the block's section :class:`~pcapkit.foundation.engines.pcapng.Context`, which it returned from 1.0.0 through 1.4.x, is :attr:`.PCAPNG.section_context`, with no alias, and ``context`` is - the caller supplied :class:`~pcapkit.corekit.context.ContextRegistry` every protocol + the caller-supplied :class:`~pcapkit.corekit.context.ContextRegistry` every protocol inherits from :class:`.ProtocolBase`, on a Section Header Block too. - :attr:`.PCAPNG.section_context` raises :exc:`.UnsupportedCall` on a Section Header - Block, as ``context`` did. Code reading ``block.context`` for the section reads + :attr:`.PCAPNG.section_context` raises :exc:`.UnsupportedCall` where ``context`` did: + on a block that holds no section, such as a Section Header Block the + :class:`~pcapkit.foundation.engines.pcapng.PCAPNG` engine has not assigned the section + it opens. Code reading ``block.context`` for the section reads ``block.section_context`` (:issue:`2172`, :pr:`2205`). Fixed @@ -1944,8 +1945,9 @@ Fixed seeds ``self.Flags(0)``, as the construct path already did at each of its sites. In the dump, ``__value__`` was a JSON *number* for a flagless frame and a *string* for every other frame in the same capture; it is consistently a string now, - ``"Flags::None [0]"`` where it read ``0``, the same change :issue:`616` made to - TCP's ``connection``. The seed is guarded because :class:`.FrameType.Flags` declares + ``"Flags::0 [0]"`` (the spelling the :mod:`pcapkit.dumpkit` fix above gives a + nameless member) where it read ``0``, the same change :issue:`616` made to TCP's + ``connection``. The seed is guarded because :class:`.FrameType.Flags` declares no members and, from Python 3.11, a memberless ``enum.Flag`` subclass refuses ``Flags(0)``, so the frame schemas that inherit it (:class:`~pcapkit.protocols.schema.application.httpv2.UnassignedFrame`, @@ -2236,7 +2238,7 @@ Fixed them byte-identical. So that a resolved unassigned member still pickles, ``_unregistered_member`` installs a ``__reduce_ex__`` rebuilding an equivalent unregistered member (:issue:`575`). -* ``httpv2._guess_version`` tried to parse a stream as HTTP/2 and read failure as "not +* :meth:`.HTTP._guess_version` tried to parse a stream as HTTP/2 and read failure as "not HTTP/2", rather than identifying the version first, so a genuine connection preface followed by a real ``SETTINGS`` frame raised ``ProtocolError: unknown HTTP version``. It now identifies before it parses: a preface is recognised as HTTP/2 outright, @@ -2704,7 +2706,11 @@ Fixed address not packed as IPv4 or IPv6 that is none of those and not an ``ipaddress`` address, raises :exc:`.ProtocolError`, where both failed later with a bare ``TypeError``. A ``bytearray`` packs as ``bytes`` (:issue:`2166`, :pr:`2182`, - :issue:`2256`). + :issue:`2256`, :pr:`2257`). +* Ethernet's and PCAP-NG's MAC address makers, and PCAP-NG's EUI address maker, raise + :exc:`.ProtocolError` for an address that is none of ``str``, ``bytes`` and + ``bytearray`` (a ``bool`` included), where they raised a bare ``TypeError`` + (:issue:`2222`, :pr:`2232`). * the Quick-Start rate parses as the ``int`` the IPv4, HOPOPT, IPv6-Opts and TCP data models declare, where it was a ``float`` such as ``80.0``, and a TCP Fast Open cookie request, the option of length 2, parses with ``cookie`` as ``b''`` rather than @@ -2850,8 +2856,7 @@ Added * :mod:`pcapkit.utilities.datetime`, holding :func:`~pcapkit.utilities.datetime.fromtimestamp` and :func:`~pcapkit.utilities.datetime.totimestamp`, the timestamp conversions of the - capture-timestamp entry under :mod:`pcapkit.protocols` (:pr:`2111`, :pr:`2136`). Its - first name, ``pcapkit.utilities.timestamp``, shipped in no release. + capture-timestamp entry under :mod:`pcapkit.protocols` (:pr:`2111`, :pr:`2136`). Changed ~~~~~~~ @@ -2979,14 +2984,14 @@ Fixed instance-method-only override fails the *class* call ``_snapshot_and_restore`` makes but not the *instance* call inside ``__init__``, so that target runs unprotected and **succeeds silently**. **3.** The - ``# pylint: disable=no-else-raise`` pragma is **kept**: the belief that R1720 never - applies to ``try``/``except``/``else`` was a message-control artefact - (``--disable=all --enable=no-else-raise`` does not re-enable the check the way - ``--enable=R`` does), and under the ``Makefile``'s flags, which CI runs, pylint flags - the ``else:``, which is deliberate because it makes the backup cleanup unreachable - from the ``except``. Left out of scope: an untested ``os.close(fd)`` leak (harmless, - since ``copy2`` reopens the path) and the semantically identical unindented form of - the same ``else:``. Closes :issue:`875` (:pr:`876`). + ``# pylint: disable=no-else-raise`` pragma is **kept**, because it is needed: under + the ``Makefile``'s flags, which CI runs, pylint reports R1720 on the ``else:``. A + ``--disable=all --enable=no-else-raise`` run reports nothing there, since enabling + the one message does not re-enable the check on a ``try``/``except``/``else`` the way + ``--enable=R`` does, so such a run cannot show the pragma unneeded. The ``else:`` is + deliberate: it makes the backup cleanup unreachable from the ``except``. Left out of + scope: an untested ``os.close(fd)`` leak (harmless, since ``copy2`` reopens the path) + and the semantically identical unindented form of the same ``else:``. Closes :issue:`875` (:pr:`876`). * :class:`~pcapkit.const.pcapng.tls_key_label.TLSKeyLabel` tracked the Mozilla NSS key-log wiki, but ``draft-ietf-opsawg-pcapng-06`` section 4.7 now points at ``[I-D.ietf-tls-keylogfile]`` (draft-05), published as :rfc:`9850`, "The SSLKEYLOGFILE @@ -3270,6 +3275,14 @@ Changed entries once the ruleset is updated separately, as a repository setting outside the change; the ``Compat`` names are excluded from the replacement. Only ``.github/workflows/unit-tests.yml`` changes; no library or test code (:pr:`856`). +* ``engine-tests`` runs one job per engine, ``Engines ()``, each looping over + its interpreters in fresh virtual environments, in place of :pr:`849`'s job per + Python and engine. ``required-checks`` also needs ``changes`` and ``project-tests``: + ``project-tests`` must pass on every diff, and a skipped leg passes only where + ``changes`` classifies the diff as docs-only (:issue:`1052`, :pr:`1055`), which may + skip ``test``, ``integration``, ``engine-tests`` and ``pypcap-parity``, or as + test-only, which may skip ``engine-tests`` and ``pypcap-parity`` (:issue:`2243`, + :pr:`2244`). * the prose the post-:issue:`719` defect wave added or touched, checked against the code; no code changes. :attr:`.MesgIDOption.seconds` in the Mobility Header schema said seconds since 1970 where it holds an :rfc:`1305` NTP count from 1900; @@ -3406,9 +3419,9 @@ Fixed choosealicense.com's template, whose first line is that title and whose notice reads ``Copyright (c) [year], [fullname]``. The licence body is unaltered: the change is confined to the copyright line, and the ``DAMAGE.`` that :pr:`615` repaired from ``DAMAGE.s`` is still clean (:pr:`630`). -* ``CITATION.cff`` shipped in no source distribution. ``MANIFEST.in`` has an ``include`` +* ``CITATION.cff`` shipped in no source distribution. ``MANIFEST.in`` had an ``include`` each for ``README.md``, ``LICENSE`` and ``CHANGELOG.md`` but none for the citation - file, and its ``global-include`` patterns (``*.rst``, ``*.py``) cannot match a + file, and its ``global-include`` patterns (``*.rst``, ``*.py``) could not match a ``.cff``. Removing those ``include`` lines and rebuilding shows ``README.md`` and ``LICENSE`` shipping anyway (setuptools adds the latter from ``license_files``, recorded as ``License-File: LICENSE`` in ``PKG-INFO``) while ``CHANGELOG.md``