From 0ffba1f6ac91728151e75fc5545a4e93f5829041 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Thu, 15 Oct 2026 18:39:18 -0400 Subject: [PATCH] docs(changelog): second whole-repo pass over the changelog (#1103) Entries: - add #1791 (breaking: pack() leaves its instance unchanged), #1778 (Protocol in protocol.__all__), #1781 (PortEnumField miss with a nameless proto), #1779 (vendor.ipx _path), #1780 (examples READMEs to Markdown, build leftovers) and #1790 (36 ambiguous cross-references) - fold #1776 into the RPL 0x23 entry and #1381 into the dump-schema one Accuracy: - the benchmark and legacy_smoke READMEs are README.md since #1780 - EngineBase stays out of __all__, as #1016 settled - the #575 peek calls get() only once the value resolves - the #647 and #764 entries point at #775/#860, under which nothing mints - the #861 entry names #869 and #874 for the EnumRegistry move Figures: - preamble: 685 citations, #340 to #1791 - process.rst pin: 298 entries; CHANGELOG.md regenerated Refs #1103 --- CHANGELOG.md | 26 ++++--- docs/source/changelog/1.5.0.rst | 76 ++++++++++++++----- .../contributing/conventions/process.rst | 2 +- 3 files changed, 76 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72566b77d..b25003ec2 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. Three more 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 669 issues and pull requests from [#340](https://github.com/JarryShaw/PyPCAPKit/pull/340) to [#1754](https://github.com/JarryShaw/PyPCAPKit/pull/1754). +The largest release since 1.0, and the first recorded here as it happened rather than reconstructed. Three more 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 685 issues and pull requests from [#340](https://github.com/JarryShaw/PyPCAPKit/pull/340) to [#1791](https://github.com/JarryShaw/PyPCAPKit/pull/1791). 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.0b12` (2026-09-20 to 2026-10-13), 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)). @@ -25,13 +25,13 @@ Preceded by `1.5.0a1` (2026-09-15), `1.5.0b1` to `1.5.0b3` (all 2026-09-19) and - the string-keyed `get()` in `pcapkit.const.ftp.command` and `pcapkit.const.http.method` tested membership with the raw key but registered `key.upper()`, so the *first* lowercase or mixed-case token raised `TypeError: 'RETR' already in use`. Reachable from wire data: `pcapkit.protocols.application.ftp` matches with `re.I` and passes the match verbatim, and [RFC 959 Section 5](https://datatracker.ietf.org/doc/html/rfc959#section-5) makes FTP commands case-insensitive, so `retr file.txt` was a valid request the library could not parse. `get()` and `_missing_` now look the key up under the canonical upper-case name, so every casing resolves to the existing member; a duplicate `GET` would have lacked the real one's `safe` and `idempotent` attributes ([#582](https://github.com/JarryShaw/PyPCAPKit/issues/582), [#583](https://github.com/JarryShaw/PyPCAPKit/issues/583)). `http.method` no longer folds case: [RFC 9110 Section 9.1](https://datatracker.ietf.org/doc/html/rfc9110#section-9.1) makes the method token case-sensitive, so `Method.get` matches it exactly ([#896](https://github.com/JarryShaw/PyPCAPKit/issues/896), below). - `get()`'s documented `default` was ignored on the integer path across the generated `pcapkit.const` tree, because `_missing_` cannot see the caller's `default` -- `Hardware.get(99999, 0)` raised `ValueError` instead of returning `0`. The integer path now consults `default` before letting the error escape. `-1`, the placeholder the generated signature already carried, still separates "no default" from "a default"; [#859](https://github.com/JarryShaw/PyPCAPKit/pull/859) replaced it with an identity sentinel once the shared base [#858](https://github.com/JarryShaw/PyPCAPKit/pull/858) (below) put `get()` behind one call site. The sweep covers 110 of the 118 integer registries, not the three [#584](https://github.com/JarryShaw/PyPCAPKit/issues/584) named; `pcapng` `OptionType` and `reg` `AppType` carry a bespoke integer fallback and are deliberately left alone, since neither drops a default by raising. Not reachable from wire data, so a contract fix rather than a parse fix ([#584](https://github.com/JarryShaw/PyPCAPKit/issues/584)). - `_missing_` in the four Mobility Header flag enumerations (`BindingACKFlag`, `BindingUpdateFlag`, `HandoverACKFlag`, `HandoverInitiateFlag`) ended in `return cls(value)`, the constructor that had just failed, so every in-range value that is not already a member re-entered `_missing_` unbounded and raised `RecursionError` -- `F(0)`, no flags set, and every composite such as `BindingACKFlag(0x06)`. Defining `_missing_` at all caused it, by shadowing the `aenum` `Flag` machinery that resolves exactly those values; `pcapkit/const/tcp/flags.py` had none and so never had the defect (it has one now, from [#647](https://github.com/JarryShaw/PyPCAPKit/issues/647) below, with the same tail). All four now end in `return super()._missing_(value)`, as `pcapkit/vendor/default.py` emits and most const modules ([#647](https://github.com/JarryShaw/PyPCAPKit/issues/647)'s two new ones included) already did, so `F(0)` is an empty flag and `BindingACKFlag(0x06)` is `S|D`. Minting through `extend_enum` was rejected for a flag type: naming `0x06` `Unassigned_0x06` would hide the composite and add a member per bit pattern. The range guard is untouched ([#623](https://github.com/JarryShaw/PyPCAPKit/issues/623)). -- six of the 123 constant registries rejected an invalid value differently from the built-in `enum`, and three did not reject it at all. `Flags`, `ftp.command.CommandType` and `reg.apptype.TransportProtocol` are `IntFlag` types with no `_missing_`, so `aenum` composed a pseudo-member for any integer: `Flags(-1)` returned `65520` (every TCP flag set, from a value no 16-bit field can hold) and `Flags(-65536)` none. `ftp.command.Command`, `FEATCode` and `http.method.Method` are `StrEnum` types whose `_missing_` called `value.upper()` before checking the type, so an integer raised `AttributeError`. All six now raise a bare `ValueError`, as `enum.IntEnum` does and 113 of the 117 modules already did. [#647](https://github.com/JarryShaw/PyPCAPKit/issues/647)'s proposal to replace those 113 with `pcapkit.utilities.exceptions.EnumError` was deliberately rejected: `EnumError` is `(BaseError, TypeError)`, not a `ValueError`, so it would diverge from the built-in, walk past the `except ValueError` in all 113 generated `get()` bodies (silently undoing [#584](https://github.com/JarryShaw/PyPCAPKit/issues/584)), and log CRITICAL from `BaseError.__init__` once per discarded default. The one deliberate divergence is kept and pinned: mutable registries look up, miss, and `extend_enum` rather than raising, so `Method('FROBNICATE')`, `ProtectionAuthority(1 << 70)` and `AppType.get(65000, proto=tcp)` still register. The three flag guards end in `return super()._missing_(value)`, so in-range composites still decompose and [#623](https://github.com/JarryShaw/PyPCAPKit/issues/623) stays fixed; `Flags` accepts `0 <= value <= 0xFFFF`. A new `tests/const/test_const_enum_builtin_parity.py` asserts that every one of the 123 registries raises the same exception type as a stdlib `enum.IntEnum`; it keys on `aenum.Enum` because an `IntFlag` is not an `IntEnum` subclass, which is how three of the six escaped the existing sweeps ([#647](https://github.com/JarryShaw/PyPCAPKit/issues/647)). -- **a breaking change to** `AppType.get`: an out-of-range port is refused rather than minted. Its `except ValueError` caught the range rejection from `_missing_` and minted regardless, so `AppType.get(-1, proto='tcp')` returned an unreachable `PORT_-1_tcp` while `TCP(-1)` raised. It now tests `_missing_` for `None` ("no row for this port") and lets the rejection through; a valid but unassigned port still mints. `Transport._make_port` **passes an unvalidated** `int`, **so** `TCP.make(srcport=99999)` **now raises where it minted junk.** Separately, the crawler rendered one `_missing_` branch per registry row, so the two spans IANA registers on more than one transport emitted the same condition twice and every registry answered with the first: `AppType.get(6010, proto='udp')` gave a member whose `proto` read `tcp`, `6666/udp` answered `ircu` rather than `reserved`, and `proto='sctp'` minted `x11` where IANA assigns nothing. A branch naming a transport now tests `cls.__transport__`; the 762 naming none are unchanged ([#764](https://github.com/JarryShaw/PyPCAPKit/pull/764)). +- six of the 123 constant registries rejected an invalid value differently from the built-in `enum`, and three did not reject it at all. `Flags`, `ftp.command.CommandType` and `reg.apptype.TransportProtocol` are `IntFlag` types with no `_missing_`, so `aenum` composed a pseudo-member for any integer: `Flags(-1)` returned `65520` (every TCP flag set, from a value no 16-bit field can hold) and `Flags(-65536)` none. `ftp.command.Command`, `FEATCode` and `http.method.Method` are `StrEnum` types whose `_missing_` called `value.upper()` before checking the type, so an integer raised `AttributeError`. All six now raise a bare `ValueError`, as `enum.IntEnum` does and 113 of the 117 modules already did. [#647](https://github.com/JarryShaw/PyPCAPKit/issues/647)'s proposal to replace those 113 with `pcapkit.utilities.exceptions.EnumError` was deliberately rejected: `EnumError` is `(BaseError, TypeError)`, not a `ValueError`, so it would diverge from the built-in, walk past the `except ValueError` in all 113 generated `get()` bodies (silently undoing [#584](https://github.com/JarryShaw/PyPCAPKit/issues/584)), and log CRITICAL from `BaseError.__init__` once per discarded default. The one deliberate divergence is kept and pinned: mutable registries look up, miss, and `extend_enum` rather than raising, so `Method('FROBNICATE')`, `ProtectionAuthority(1 << 70)` and `AppType.get(65000, proto=tcp)` still register; the [#775](https://github.com/JarryShaw/PyPCAPKit/issues/775) and [#860](https://github.com/JarryShaw/PyPCAPKit/issues/860) entries below record how all three came to return an unregistered member instead. The three flag guards end in `return super()._missing_(value)`, so in-range composites still decompose and [#623](https://github.com/JarryShaw/PyPCAPKit/issues/623) stays fixed; `Flags` accepts `0 <= value <= 0xFFFF`. A new `tests/const/test_const_enum_builtin_parity.py` asserts that every one of the 123 registries raises the same exception type as a stdlib `enum.IntEnum`; it keys on `aenum.Enum` because an `IntFlag` is not an `IntEnum` subclass, which is how three of the six escaped the existing sweeps ([#647](https://github.com/JarryShaw/PyPCAPKit/issues/647)). +- **a breaking change to** `AppType.get`: an out-of-range port is refused rather than minted. Its `except ValueError` caught the range rejection from `_missing_` and minted regardless, so `AppType.get(-1, proto='tcp')` returned an unreachable `PORT_-1_tcp` while `TCP(-1)` raised. It now tests `_missing_` for `None` ("no row for this port") and lets the rejection through; a valid but unassigned port still minted, until [#874](https://github.com/JarryShaw/PyPCAPKit/pull/874) (below) made it an unregistered member. `Transport._make_port` **passes an unvalidated** `int`, **so** `TCP.make(srcport=99999)` **now raises where it minted junk.** Separately, the crawler rendered one `_missing_` branch per registry row, so the two spans IANA registers on more than one transport emitted the same condition twice and every registry answered with the first: `AppType.get(6010, proto='udp')` gave a member whose `proto` read `tcp`, `6666/udp` answered `ircu` rather than `reserved`, and `proto='sctp'` minted `x11` where IANA assigns nothing. A branch naming a transport now tests `cls.__transport__`; the 762 naming none are unchanged ([#764](https://github.com/JarryShaw/PyPCAPKit/pull/764)). - **a breaking change to** `AppType._dispatch`: it resolved a `proto` naming several transports by its lowest set bit, and `tcp` is lowest, so every composite containing it dispatched into the TCP registry -- silently, and undetectably to a caller checking equality, since `AppType.__eq__` compares on `port` alone. `TransportProtocol` was still an `aenum.IntFlag` and a member's `proto` carried the whole set IANA assigned the service, so `tcp | udp` was an ordinary value to pass back in; the `AppType.proto` and `TransportProtocol` entries above record how [#806](https://github.com/JarryShaw/PyPCAPKit/issues/806) and [#808](https://github.com/JarryShaw/PyPCAPKit/issues/808) later retired both. Over every multi-transport member this misrouted two: port 888 (`accessbuilder` resolved to TCP's `cddbp`) and port 999 (`puprouter` resolved to TCP's `garcon`). It now raises `ProtocolError`, a `ValueError` subclass, so the documented contract holds. No library caller passes a composite ([#759](https://github.com/JarryShaw/PyPCAPKit/issues/759)). - `get()`'s string-key miss and every generated registry's `_missing_` bounded-range branch called `aenum.extend_enum()` on an unrecognised value, so e.g. `Hardware(40)` minted a permanent member. Fixed at the two shared sites in `pcapkit/vendor/default.py`, plus a new `register(value, name)` base classmethod for the caller-named path that still mints. All 105 generated registries are updated; the non-minting `get()` miss applies to all 105, but the `_missing_` fix changes behaviour only for the **21** with a bounded-range `extend_enum` branch. `pcapkit/toolkit/scapy.py` and `pcapkit/toolkit/pyshark.py` fed `get()` a string key with no default and now raise `MissingKeyError` instead of receiving a placeholder; four reader sites in `hopopt.py`/`ipv6_opts.py` convert the `KeyError` into `ProtocolError`. Riding along: `pcapkit/toolkit/pyshark.py` gains a two-entry `FILTER_NAME_TO_LINKTYPE` (`eth` to `ETHERNET`, `tr` to `IEEE802_5`), consulted ahead of `LinkType.get` since pyshark reports a filter name, not a DLT name. The other minting call sites in `_missing_`, `get` and the hand-written `mh.py` and `ngap.py` were deferred; the 106 `register`/`register_alias` sites are not deferred but the by-design caller-named path ([#838](https://github.com/JarryShaw/PyPCAPKit/pull/838)). - **a breaking change to** `pcapkit.const.ipx.socket.Socket`'s `_missing_`: three of its five archived range branches were strict subsets of a range tested earlier -- `(0x0020, 0x003F)` after `(0x0001, 0x0BB8)`, and `(0x4000, 0x4FFF)` and `(0x8000, 0xFFFF)` after `(0x0BB9, 0xFFFF)` -- so they never fired. The generator `pcapkit/vendor/ipx/socket.py` now lists each subset range ahead of the range containing it. **36891 of the 65521 values that** `_missing_` **resolves (56.3%) change name**: 32 from `Registered by Xerox` to `Experimental`, 4095 from `Dynamically Assigned` to `Dynamically Assigned Socket Numbers` and 32764 from `Dynamically Assigned` to `Statically Assigned Socket Numbers`. No defined member changes and every value still resolves ([#847](https://github.com/JarryShaw/PyPCAPKit/pull/847)). - `LinkType(209).name` returned `'IPMB_LINUX'`, which tcpdump's own table marks "Legacy names (do not use)", because a 2024-05-04 regeneration emitted that row above the current `I2C_LINUX = 209` and `aenum` gives a value to the member defined first. `pcapkit/vendor/reg/linktype.py` sinks a row whose note mentions "legacy" after every current row, so the current name is defined first; in the registry crawled for this release that affects value 209 alone. `IPMB_LINUX` stays a reachable alias (`LinkType['IPMB_LINUX']` still resolves to 209). Not breaking: only the canonical name for the shared value moves ([#848](https://github.com/JarryShaw/PyPCAPKit/pull/848)). The rule sinks only a row whose value another row also holds, counting values inside ranges, so a current row that merely mentions a legacy name stays first ([#852](https://github.com/JarryShaw/PyPCAPKit/issues/852), [#854](https://github.com/JarryShaw/PyPCAPKit/pull/854)). -- **a breaking change to** 82 more const registries' `_missing_`: applies the owner's mint/unmint ruling (settled on [#847](https://github.com/JarryShaw/PyPCAPKit/pull/847), confirmed on [#775](https://github.com/JarryShaw/PyPCAPKit/issues/775): a final, concrete assigned name mints, and a notation for readers does not) across the 89 `_missing_` files still calling `extend_enum`. 80 registries convert wholly (164 branches), each minting a bare status word (`Unassigned`, `Reserved [...]`, `Reserved for Private/Experimental Use`, `Unspecified in the IANA registry`, `Deprecated`), which the ruling treats as a notation rather than an assignment. Two are mixed: `EtherType`'s `DEC Unassigned` (3 branches) and "Old Xerox Experimental values. Invalid as an Ethertype since 1983." (1) convert, while 46 vendor-company names across 50 range blocks (Xyplex, Motorola, Walker Richer & Quinn and 43 more) keep minting, since a proprietary protocol has no public name beyond the company's; `IEEE802_3_Length_Field` and `Berkeley_Trailer_encap_IP` also keep minting but are not company names. `Socket` (ipx)'s allocation-policy names (`Experimental`, `Dynamically Assigned Socket Numbers`, `Statically Assigned Socket Numbers`, `Dynamically Assigned`) convert, while `Registered by Xerox` keeps minting, a real ownership fact. 7 of the 89 are left alone: `CGAType`'s `Tag_` mint is not an IANA-style range, and the other 6 files sit on 9 classes not yet on `EnumRegistry` (`AppType`, `StatusCode`, `ftp.return_code`, `ftp.command`, `Method`, `OptionType`), left to [#859](https://github.com/JarryShaw/PyPCAPKit/pull/859)/[#860](https://github.com/JarryShaw/PyPCAPKit/issues/860). Each conversion changes both the vendor crawler and the generated file, per [#838](https://github.com/JarryShaw/PyPCAPKit/pull/838)'s and [#858](https://github.com/JarryShaw/PyPCAPKit/pull/858)'s precedent. The worked examples in `docs/source/contributing/conventions/mint-criterion.rst` keep `Registered by Xerox` under Mint ([#861](https://github.com/JarryShaw/PyPCAPKit/pull/861)). +- **a breaking change to** 82 more const registries' `_missing_`: applies the owner's mint/unmint ruling (settled on [#847](https://github.com/JarryShaw/PyPCAPKit/pull/847), confirmed on [#775](https://github.com/JarryShaw/PyPCAPKit/issues/775): a final, concrete assigned name mints, and a notation for readers does not) across the 89 `_missing_` files still calling `extend_enum`. 80 registries convert wholly (164 branches), each minting a bare status word (`Unassigned`, `Reserved [...]`, `Reserved for Private/Experimental Use`, `Unspecified in the IANA registry`, `Deprecated`), which the ruling treats as a notation rather than an assignment. Two are mixed: `EtherType`'s `DEC Unassigned` (3 branches) and "Old Xerox Experimental values. Invalid as an Ethertype since 1983." (1) convert, while 46 vendor-company names across 50 range blocks (Xyplex, Motorola, Walker Richer & Quinn and 43 more) keep minting, since a proprietary protocol has no public name beyond the company's; `IEEE802_3_Length_Field` and `Berkeley_Trailer_encap_IP` also keep minting but are not company names. `Socket` (ipx)'s allocation-policy names (`Experimental`, `Dynamically Assigned Socket Numbers`, `Statically Assigned Socket Numbers`, `Dynamically Assigned`) convert, while `Registered by Xerox` keeps minting, a real ownership fact. 7 of the 89 are left alone: `CGAType`'s `Tag_` mint is not an IANA-style range, and the other 6 files sit on the 9 classes [#860](https://github.com/JarryShaw/PyPCAPKit/issues/860) moves onto `EnumRegistry` (`AppType`, `StatusCode`, `ftp.return_code`, `ftp.command`, `Method`, `OptionType`; [#869](https://github.com/JarryShaw/PyPCAPKit/pull/869) and [#874](https://github.com/JarryShaw/PyPCAPKit/pull/874), below). Each conversion changes both the vendor crawler and the generated file, per [#838](https://github.com/JarryShaw/PyPCAPKit/pull/838)'s and [#858](https://github.com/JarryShaw/PyPCAPKit/pull/858)'s precedent. The worked examples in `docs/source/contributing/conventions/mint-criterion.rst` keep `Registered by Xerox` under Mint ([#861](https://github.com/JarryShaw/PyPCAPKit/pull/861)). - **a breaking change to** `EtherType`'s `_missing_` ordering: values `0x0101`-`0x01FF` now resolve to their Old Xerox Experimental name instead of an `IEEE802_3_Length_Field_0x0101`-style one. The generated method tested `0x0000 <= value <= 0x05DC` (IEEE802.3 Length Field) first, and the Old Xerox range sits wholly inside it, so that branch was unreachable. Unlike [#841](https://github.com/JarryShaw/PyPCAPKit/issues/841)'s IPX fix, these bounds come from the live IANA CSV, so the fix is general: `pcapkit/vendor/reg/ethertype.py`'s `process()` collects range rows and a new `EtherType._insert_range` places each ahead of the first already-placed range that fully contains it, leaving non-overlapping pairs in CSV order; this pair is the only containment among the 56 ranges. Closes [#862](https://github.com/JarryShaw/PyPCAPKit/issues/862) ([#865](https://github.com/JarryShaw/PyPCAPKit/pull/865)). - **a breaking change to** `Method`: `__new__` called `str.__new__(cls)` with no argument, so all 40 registered members' `str` payload was empty regardless of value -- `str(Method.GET) == ''` and `Method.GET == 'GET'` was `False`; `.value` was always correct. [#869](https://github.com/JarryShaw/PyPCAPKit/pull/869) (above) had fixed the *unregistered* path only, leaving `str(Method('frob')) == 'frob'` beside `str(Method.GET) == ''`. Fixed to `str.__new__(cls, value)`, mirroring `Command.__new__`, in the generator (`pcapkit/vendor/http/method.py`) and the generated module. No sibling hand-rolled `__new__` has the defect (`OptionType`/`AppType` deliberately store a display string). Consequences, all corrections: `Method.GET == 'GET'` is `True`; `bool(Method.GET)` flips `False` to `True`; `json.dumps(Method.GET)` emits `"GET"` not `""`; `sorted()` is lexicographic rather than input-order-stable; and `{m: ... for m in Method}` gains 40 entries where it had 1, because all 40 members previously hashed as `''` and compared equal. Nothing under `pcapkit/protocols` relied on the old behaviour. Closes [#870](https://github.com/JarryShaw/PyPCAPKit/issues/870) ([#871](https://github.com/JarryShaw/PyPCAPKit/pull/871)). - **a breaking change to** `TransportProtocol` and `AppType`: step 2 of [#860](https://github.com/JarryShaw/PyPCAPKit/issues/860), PR 2 of 2 (the 8 non-`AppType` bespoke registries were [#869](https://github.com/JarryShaw/PyPCAPKit/pull/869), above). `AppType` and its four transport subclasses move onto `EnumRegistry` and both mint sites -- `_missing_`'s 766 range branches and `get()`'s second mint (`PORT_{port}_{transport}` -> `'unknown'`) -- now build through a new `_unregistered_member` override that reconstructs `svc`/`port`/`proto`, per the owner's ruling that only IANA-registered values are legitimate and that `get()` lacks the information to create new ones. All 766+1 branches convert, including 8 naming a real service assigned to a whole port span (7 distinct names: `x11` twice, `active-net`, `satvid-datalnk`, `vrml-multi-use`, `ircu`, `swx`, `flex-lm`); unlike `FEATCode`'s fix ([#869](https://github.com/JarryShaw/PyPCAPKit/pull/869), above) there is no import-time self-mutation to cure by declaring members statically, so these stay deliberately unregistered lookups. `AppType` gains a working `register()`; the base's would have built a member with `svc=''`. **The breaking part**: `TransportProtocol` moves from power-of-two values (`tcp=1, udp=2, sctp=4, dccp=8`) to sequential `auto()` (`sctp=3, dccp=4`), per the owner's follow-up ruling once `|`-joined composites stopped being parsed. `AppType.get` treats whatever `int` it is given as a whole, so the renumbering changes what specific integers *mean*: on `AppType.get(80, proto=...)`, a bare `4` resolved to `SCTP.http` on `main` and now **silently** resolves to `DCCP.unknown`, because a real member sits at `4` under both numberings; a bare `8` used to mint `DCCP.PORT_80_dccp` and now raises `ValueError`. The composed-value case is deliberately untested, per the owner's ruling that a breaking change already retired that path ([#874](https://github.com/JarryShaw/PyPCAPKit/pull/874)). @@ -94,6 +94,7 @@ Preceded by `1.5.0a1` (2026-09-15), `1.5.0b1` to `1.5.0b3` (all 2026-09-19) and - an `*IntField` given a `signed=` that contradicts its fixed sign, such as `UInt8Field(signed=True)`, raises `FieldValueError`; the argument used to be discarded silently ([#545](https://github.com/JarryShaw/PyPCAPKit/issues/545), [#549](https://github.com/JarryShaw/PyPCAPKit/pull/549)). - negative lengths raise `ProtocolError` rather than a bare stdlib error: `FieldBase.length` tells a malformed template from a negative resolved length ([#825](https://github.com/JarryShaw/PyPCAPKit/issues/825), [#827](https://github.com/JarryShaw/PyPCAPKit/pull/827)), and `NumberField` checks a negative resolved `length` and a negative `bit_length` ([#828](https://github.com/JarryShaw/PyPCAPKit/issues/828), [#829](https://github.com/JarryShaw/PyPCAPKit/pull/829), [#831](https://github.com/JarryShaw/PyPCAPKit/issues/831), [#834](https://github.com/JarryShaw/PyPCAPKit/pull/834)). - `ModuleDescriptor.klass` raises `ProtocolError` for a class name that resolves to nothing, or one never given, rather than a bare `AttributeError`; the two registry modules share one `NULL` sentinel compared by identity, where each had its own string ([#832](https://github.com/JarryShaw/PyPCAPKit/issues/832), [#833](https://github.com/JarryShaw/PyPCAPKit/issues/833), [#835](https://github.com/JarryShaw/PyPCAPKit/pull/835)). +- `PortEnumField` given a `proto` with no `.name` -- a string such as `'tcp'`, or a composite such as `TransportProtocol.tcp | TransportProtocol.udp`, which is a bare `int` -- raised `AttributeError` on a port the registry does not hold. The miss takes its transport from the registry's `__transport__`, as `AppType.get` does ([#1774](https://github.com/JarryShaw/PyPCAPKit/issues/1774), [#1781](https://github.com/JarryShaw/PyPCAPKit/pull/1781)). ### pcapkit.dumpkit @@ -103,7 +104,7 @@ Preceded by `1.5.0a1` (2026-09-15), `1.5.0b1` to `1.5.0b3` (all 2026-09-19) and - `tests/dumpkit/test_nameless_enum_rendering_unit.py` (added by [#670](https://github.com/JarryShaw/PyPCAPKit/pull/670)) carried two probes assuming every flag-enum registry is 16 bits wide: `test_scalar_return_renders_a_nameless_member_as_its_value` used one fixed tuple ending in `65536`, and `test_no_flag_registry_renders_the_literal_none` swept every registry but only against `registry(0)`. Widths differ: `ftp.command.CommandType` 3 bits, `reg.apptype.TransportProtocol` 4 (computed at runtime), the three `mh` ack/initiate flags 8, and `mh.binding_update_flag.BindingUpdateFlag` and `tcp.flags.Flags` 16. `tcp.flags.Flags(65536)` correctly raises (`0 <= value <= 0xFFFF`), so the sweep was failing on correct behaviour. The probes now derive each registry's bound from its declared members, and a new test pins that one past the widest in-field value is refused. Test-only ([#702](https://github.com/JarryShaw/PyPCAPKit/issues/702)). - the plist writer emitted mapping keys unescaped, so a key carrying `&`, `<` or `>` produced a report that is not well-formed XML. `dictdumper`'s `_append_dict` interpolates the key into `'{item}'` and calls `_encode_value` on the value only, so both branches of `pcapkit.dumpkit.common` that build a mapping now escape their keys through one `escape_key` helper -- a `MultiDict`, where [#575](https://github.com/JarryShaw/PyPCAPKit/issues/575) escaped an enum-derived key inline and nothing else, and a plain dict, where nothing was escaped. The json, tree and text writers are untouched. A non-`str` key is escaped too: a PCAP-NG decryption secrets block keys TLS key log entries by a raw bytes client random whose repr can carry all three characters. The upstream half is `JarryShaw/DictDumper#125`; this does not wait on it. The json writer's quoting defect stands, upstream as `JarryShaw/DictDumper#121` ([#772](https://github.com/JarryShaw/PyPCAPKit/issues/772)). - the JSON writer emitted mapping keys unescaped, so the report of `test.pcapng` did not parse (`Expecting ':' delimiter`): its TLS key log is keyed by a `bytes` client random whose repr holds `"` and `\`. `escape_key` in `pcapkit.dumpkit.common` now JSON-escapes keys for JSON output, as [#772](https://github.com/JarryShaw/PyPCAPKit/issues/772) made it XML-escape them for plist, and the JSON reports of all 23 `examples/captures/` files now parse. The upstream defect, `JarryShaw/DictDumper#121`, stands ([#1152](https://github.com/JarryShaw/PyPCAPKit/issues/1152)). -- **a breaking change to** the dump schema: the output writers dropped or corrupted what the packets carried. An `OrderedMultiDict`, which every option list is, now dumps as a list of single-key entries in wire order ([#1305](https://github.com/JarryShaw/PyPCAPKit/pull/1305)). +- **a breaking change to** the dump schema: the output writers dropped or corrupted what the packets carried. An `OrderedMultiDict`, which every option list is, now dumps as a list of single-key entries in wire order ([#1305](https://github.com/JarryShaw/PyPCAPKit/pull/1305)). Inside an object the writers fall back on, a class, module or function is written as its qualified name rather than expanded, and an object met again on its own expansion path as `` ([#1372](https://github.com/JarryShaw/PyPCAPKit/issues/1372), [#1381](https://github.com/JarryShaw/PyPCAPKit/pull/1381)). - tree output raised on a `complex` value, and is now documented as lossy ([#1266](https://github.com/JarryShaw/PyPCAPKit/issues/1266), [#1366](https://github.com/JarryShaw/PyPCAPKit/pull/1366)). - plist dates are written to whole seconds, so `plistlib` reads them back ([#1448](https://github.com/JarryShaw/PyPCAPKit/issues/1448), [#1450](https://github.com/JarryShaw/PyPCAPKit/pull/1450)). - a PCAP trace record scales its fraction to the trace's resolution ([#1500](https://github.com/JarryShaw/PyPCAPKit/issues/1500), [#1529](https://github.com/JarryShaw/PyPCAPKit/pull/1529)), and writes the octets it has rather than a truncated record's claimed length ([#1541](https://github.com/JarryShaw/PyPCAPKit/issues/1541), [#1543](https://github.com/JarryShaw/PyPCAPKit/pull/1543)). @@ -126,7 +127,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** `Extractor.register_engine`, `register_reassembly` and `register_traceflow`, and the `register_extractor_*` wrappers over them: each now accepts only a subclass of the public `Engine`, `Reassembly` or `TraceFlow` ([#1016](https://github.com/JarryShaw/PyPCAPKit/issues/1016)). They had accepted a subclass of the matching `*Base` class since [#513](https://github.com/JarryShaw/PyPCAPKit/issues/513), which was a tolerance for pcapkit's own built-ins (all 13 derive from the base, none from the public class) rather than a contract: `*Base` is documented as internal, and the public class is the one carrying the registration hook. Third-party code that subclasses `EngineBase`, `ReassemblyBase` or `TraceFlowBase` directly and registers through these functions now gets `RegistryError` naming the public class; subclass the public class instead. The error messages are unchanged. The built-ins never pass through a registrar; `Extractor._register_internal_engine` and its two siblings check the base for internal registration, and are what the [#513](https://github.com/JarryShaw/PyPCAPKit/issues/513) regression test now exercises. `register_protocol` is untouched ([#1020](https://github.com/JarryShaw/PyPCAPKit/pull/1020)). - extraction is around 46% faster on a 1,117-frame HTTP capture, with byte-identical output ([#420](https://github.com/JarryShaw/PyPCAPKit/pull/420)). A reassembled datagram's payload is analysed on first read rather than eagerly, cutting IP reassembly's own cost by 90.7% and TCP's by 23.7% (IP reassembly submits a datagram for every frame, fragmented or not) ([#424](https://github.com/JarryShaw/PyPCAPKit/pull/424)). Flow tracing over the same capture went from 1416.6 ms to 744.0 ms, because the flow dumper handed each record to a `Frame` constructor that re-dissected the whole stack to return the bytes it had just been given; options are no longer parsed twice either ([#427](https://github.com/JarryShaw/PyPCAPKit/pull/427)). - **a breaking change to** `Extractor.engine` and `Extractor.record_header`: both are now declared to return `EngineBase[_P]` rather than `Engine`, and the private `_exeng` attribute follows. The built-in engines subclass `EngineBase` directly, so none was ever an `Engine`, which adds only the `__init_subclass__` registration hook. Two things are visible to a type checker. Code passing the result to an `Engine`-typed parameter must now accept `EngineBase`; and because the old declarations were *unparameterised*, `engine.read_frame()` and `record_header().read_frame()` used to be `Any` and are now the extractor's own frame type, so an assignment that relied on `Any` there no longer type-checks ([#1022](https://github.com/JarryShaw/PyPCAPKit/issues/1022)). -- the documentation of the engine base classes: the member stubs (`name`, `module`, `registry`, `extractor`, `unsupported_reason`, `run`, `read_frame`, `close` and the rest) moved from `Engine` to `EngineBase`, where they are defined, and `Engine` now documents only the `__init_subclass__` registration hook it adds, so the `EngineBase[_P]` that `Extractor.engine` returns leads to an API. Cross-references to `Engine.run`, `Engine.read_frame`, `Engine.close` and `Engine.unsupported_reason` now point at `EngineBase`. Exporting `EngineBase` in `__all__` is left to [#1016](https://github.com/JarryShaw/PyPCAPKit/issues/1016) ([#1024](https://github.com/JarryShaw/PyPCAPKit/issues/1024)). +- the documentation of the engine base classes: the member stubs (`name`, `module`, `registry`, `extractor`, `unsupported_reason`, `run`, `read_frame`, `close` and the rest) moved from `Engine` to `EngineBase`, where they are defined, and `Engine` now documents only the `__init_subclass__` registration hook it adds, so the `EngineBase[_P]` that `Extractor.engine` returns leads to an API. Cross-references to `Engine.run`, `Engine.read_frame`, `Engine.close` and `Engine.unsupported_reason` now point at `EngineBase`, which stays out of `__all__`, as [#1016](https://github.com/JarryShaw/PyPCAPKit/issues/1016) settled ([#1024](https://github.com/JarryShaw/PyPCAPKit/issues/1024)). - **a breaking change to** `register_mh_extension`: `schema` is keyword-only, as in the other fourteen schema-taking registrars ([#516](https://github.com/JarryShaw/PyPCAPKit/issues/516), [#526](https://github.com/JarryShaw/PyPCAPKit/pull/526)). #### Fixed @@ -202,6 +203,7 @@ Layer-limited extraction: `layer='link'` no longer sets the termination flag on - **a breaking change to** a declared length that runs past the data: it is refused with `ProtocolError` rather than accepted and zero-filled on rebuild, so a malformed option looks different in `info` ([#1325](https://github.com/JarryShaw/PyPCAPKit/issues/1325), [#1398](https://github.com/JarryShaw/PyPCAPKit/pull/1398)). - dissection is bounded: a frame at 64 layers ([#1610](https://github.com/JarryShaw/PyPCAPKit/issues/1610), [#1620](https://github.com/JarryShaw/PyPCAPKit/pull/1620)) and an IPv6 extension-header chain at 32 headers ([#1604](https://github.com/JarryShaw/PyPCAPKit/issues/1604), [#1608](https://github.com/JarryShaw/PyPCAPKit/pull/1608)). Both limits are `PCAPKIT_FRAME_LAYER_LIMIT` and `PCAPKIT_EXTENSION_HEADER_LIMIT` in `pcapkit.utilities.config` ([#1743](https://github.com/JarryShaw/PyPCAPKit/pull/1743)). Rebuilding a frame cut at the chain bound warns once ([#1619](https://github.com/JarryShaw/PyPCAPKit/issues/1619), [#1630](https://github.com/JarryShaw/PyPCAPKit/pull/1630)). - **a breaking change to** IPv6 extension headers: an unrecognised or unparsable one is parsed generically, per [RFC 6564](https://datatracker.ietf.org/doc/html/rfc6564), rather than collapsing the packet to `Raw` ([#891](https://github.com/JarryShaw/PyPCAPKit/issues/891), [#904](https://github.com/JarryShaw/PyPCAPKit/pull/904)). `IPv6_Ext` does it, and is also the shared base of all eight extension headers ([#917](https://github.com/JarryShaw/PyPCAPKit/issues/917), [#924](https://github.com/JarryShaw/PyPCAPKit/pull/924)). +- **a breaking change to** `pack()`: it no longer keeps the header it builds on the instance it is called on, which in 1.4.1 too left `schema` disagreeing with `info`: `UDP(srcport=1234).pack()` reset `schema.srcport` to 0 while `info.srcport` kept 1234. Once an instance has been parsed or constructed, `pack()` builds on a shallow copy, so `schema`, `info` and what `make()` sets, such as TCP's `connection`, stay as they were. The `Frame`, `PCAPNG` and `HTTPv2` overrides do the same, and re-packing a PCAP-NG block that carries an option allowed only once no longer raises. Code that relied on `pack()` to re-sync `schema` calls `make()` or constructs a new instance ([#1769](https://github.com/JarryShaw/PyPCAPKit/issues/1769), [#1791](https://github.com/JarryShaw/PyPCAPKit/pull/1791)). #### Fixed @@ -247,7 +249,7 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r - `httpv2.HTTP.read` tested only the 24-bit declared length off the wire (`schema.length < 9`), never how many octets the buffer held, so a frame backed by far fewer real octets than declared parsed and reported the declared, attacker-controlled length. Sweeping the declared length against a four-octet buffer gave non-monotone accept/reject (9, 15, 65535 and 16777215 parsed, 10 and 100 did not), an artifact of `_read_http_settings`'s own `(declared - 9) % 6` check. `HTTP.unpack` now rejects a non-empty buffer under nine octets before the schema layer runs, preserving `StreamEOFError` for a genuinely exhausted implicit-length stream, and `read`'s guard also requires the declared length not to exceed the buffer. A buffer that clears nine octets can still carry a frame type whose fixed fields exceed what is left (GOAWAY at 9-16 octets, PUSH_PROMISE at 9-12, padded DATA, HEADERS and PUSH_PROMISE) and crash the schema layer with a bare `struct.error`; that root cause is in the generic `Schema.unpack`/ `FieldBase.length` machinery shared by ten schema modules, out of scope and filed as [#805](https://github.com/JarryShaw/PyPCAPKit/issues/805), which is why `_guess_version`'s last arm keeps suppressing `struct.error` alongside `ProtocolError` ([#802](https://github.com/JarryShaw/PyPCAPKit/pull/802)). - `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)`; a missing or non-`b'\n'` octet ends the entry with a `SchemaWarning` instead of resynchronising at the wrong offset. `test_journal_fields_following_a_binary_field_are_not_discarded` fails against the unfixed code 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. Walking the entry instead of splitting it introduced a regression on first review: a `malformed` flag broke the *outer* per-block loop on a bad terminator, ending the whole block and dropping every well-formed entry behind it. Fixed by dropping `malformed`, leaving the per-entry `break` as the only effect. A second 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** four `.get()`-backed enum fields: TCP/UDP/SCTP's `PortEnumField` and PCAP-NG's `OptionEnumField` minted a member through `aenum.extend_enum` for every value no row or documented span covers (111 calls on `http.pcap`, all ephemeral ports). All four now peek at what `.get()` would consult and mint only once the value is unresolvable; 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 6 of the other 7 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: 14 of 23 captures change, entirely from the new `` rendering plus the PLIST escaping above; normalising both leaves all 14 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)). +- **a breaking change to** four `.get()`-backed enum fields: TCP/UDP/SCTP's `PortEnumField` and PCAP-NG's `OptionEnumField` minted a member through `aenum.extend_enum` for every value no row or documented span covers (111 calls on `http.pcap`, all ephemeral ports). All four now peek 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 6 of the other 7 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: 14 of 23 captures change, entirely from the new `` rendering plus the PLIST escaping above; normalising both leaves all 14 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 first two 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 nine 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** 8 more bespoke registries: step 2 of [#860](https://github.com/JarryShaw/PyPCAPKit/issues/860), PR 1 of 2 (`AppType` is PR 2, [#874](https://github.com/JarryShaw/PyPCAPKit/pull/874) below), 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_`. The first five convert 12 unambiguous placeholder branches (`Unassigned`, `Unknown`, `opt_unknown`) to `_unregistered_member`; the three 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, but a round-2 review found it still minted on both its int/namespace and `str` paths, and the live pcapng parse path (`PCAPNG._make_pcapng_options`) calls it with wire bytes, so parsing an undeclared option code still registered a permanent member; both paths now build an unregistered 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()` (a second, independent mint site) now build an unregistered member. `FEATCode`'s crawler now declares all 15 real FEAT-code values from the live IANA table instead of minting 10 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 PR 2'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 two paths gets both pickling and correct rendering right at once ([#869](https://github.com/JarryShaw/PyPCAPKit/pull/869)). @@ -276,7 +278,7 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r - **a breaking change to** the SMF_DPD option's maker in `HOPOPT` and `IPv6_Opts`: it overwrote the parsed DPD mode, so H-DPD rebuilt as I-DPD, and inferred the TaggerID type from the Tagger ID's length, so a 4-octet `DEFAULT` Tagger ID rebuilt as IPv4. Both now come from the parsed option, or from `mode` and a new `tid_type` keyword. A Tagger ID that does not fit its type raises `ProtocolError`, so a 17-octet one does where it raised `FieldValueError`, and `tid=b''` builds a NULL Tagger ID where it raised `ValueError` ([#1190](https://github.com/JarryShaw/PyPCAPKit/issues/1190)). An H-DPD option with an empty Hash Assist Value raises `ProtocolError` where it raised a bare `IndexError` ([#1193](https://github.com/JarryShaw/PyPCAPKit/issues/1193)). - a `ForwardMatchField` that read short at the end of the stream rewound by its declared width, not by the octets it read, so the fields after it re-read the preceding octet. An SMF_DPD option in the last two octets of a `HOPOPT` or `IPv6_Opts` header was re-read from the padding and raised a bare `TypeError`; it now raises `FieldValueError`, as CALIPSO and MPL do. A full read rewinds as before ([#1195](https://github.com/JarryShaw/PyPCAPKit/issues/1195)). - `HIP()` with no parameters wrote Header Length 0, below [RFC 7401](https://datatracker.ietf.org/doc/html/rfc7401) section 5.1's minimum of 4 for the two HITs, so it failed to read back with `ProtocolError`; it now writes 4. `read` names a Header Length below 4 in a new `invalid header length` message. Lengths 0 to 3 already raised `ProtocolError`, so only the message changes ([#1189](https://github.com/JarryShaw/PyPCAPKit/issues/1189)). -- the RPL option, type `0x23`, raised `AttributeError` in `HOPOPT` and `IPv6_Opts` ([#1196](https://github.com/JarryShaw/PyPCAPKit/issues/1196), [#1198](https://github.com/JarryShaw/PyPCAPKit/pull/1198)). +- the RPL option, type `0x23`, raised `AttributeError` in `HOPOPT` and `IPv6_Opts` ([#1196](https://github.com/JarryShaw/PyPCAPKit/issues/1196), [#1198](https://github.com/JarryShaw/PyPCAPKit/pull/1198)). The test that an option of any type in the last two octets of the header fails in-library no longer skips it; test-only ([#1771](https://github.com/JarryShaw/PyPCAPKit/issues/1771), [#1776](https://github.com/JarryShaw/PyPCAPKit/pull/1776)). - **a breaking change to** the RPL option: it keeps its reserved flag bits on rebuild and reads [RFC 6553](https://datatracker.ietf.org/doc/html/rfc6553) sub-TLVs ([#1199](https://github.com/JarryShaw/PyPCAPKit/issues/1199), [#1201](https://github.com/JarryShaw/PyPCAPKit/pull/1201)). - `make()` rejected an enum member name given as `str` unless a namespace was passed ([#1210](https://github.com/JarryShaw/PyPCAPKit/issues/1210), [#1227](https://github.com/JarryShaw/PyPCAPKit/pull/1227)); raw items in a `make()` list build a packet, and `str` member names resolve per argument ([#1352](https://github.com/JarryShaw/PyPCAPKit/pull/1352)). - **a breaking change to** rebuilding a parsed packet, the round-trip programme: `from_data(info)` gives back the packet's own octets. Most of these fixes add the `make()` keywords and `info` keys the rebuild needs, which is what breaks: @@ -311,6 +313,7 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r - IPv4's SEC option sets the termination bit its own reader requires, sizes the bitmap from the bit count, and SID is 16 bits wide ([#534](https://github.com/JarryShaw/PyPCAPKit/issues/534), [#537](https://github.com/JarryShaw/PyPCAPKit/issues/537), [#543](https://github.com/JarryShaw/PyPCAPKit/pull/543)). - the MPTCP option header reads `length` from bit 8, per [RFC 8684 Section 3](https://datatracker.ietf.org/doc/html/rfc8684#section-3), rather than bit 1 ([#553](https://github.com/JarryShaw/PyPCAPKit/issues/553), [#558](https://github.com/JarryShaw/PyPCAPKit/pull/558)). - four Mobility Header enumerations defined inline from their RFCs (`FastBindingAcknowledgmentStatus`, `IPv6AddressPrefixCode`, `LocalizedRoutingStatus`, `LMAAddressCode`) raise `EnumValueError` for an unassigned value rather than minting `Unassigned_N` ([#880](https://github.com/JarryShaw/PyPCAPKit/issues/880), [#889](https://github.com/JarryShaw/PyPCAPKit/pull/889)), and all four inherit `get` and its `default` from `EnumLookup` rather than overriding it ([#935](https://github.com/JarryShaw/PyPCAPKit/issues/935), [#940](https://github.com/JarryShaw/PyPCAPKit/pull/940)). +- `pcapkit.protocols.protocol.__all__` lists `Protocol` beside `ProtocolBase`, so `from pcapkit.protocols.protocol import *` binds the module's public class ([#1770](https://github.com/JarryShaw/PyPCAPKit/issues/1770), [#1778](https://github.com/JarryShaw/PyPCAPKit/pull/1778)). ### pcapkit.toolkit @@ -368,6 +371,7 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r - `pcapkit.vendor` and `pcapkit.vendor.mh` list `MH_HandoverInitiateStatus` in `__all__`, where both named `MH_HandoverACKStatus` twice, so a bare `pcapkit-vendor` run reaches its crawler ([#531](https://github.com/JarryShaw/PyPCAPKit/pull/531)). - with no terminal on stdin, a failed fetch re-raises rather than waiting for an operator to save the page, a wait that printed without end under a closed or piped stdin ([#522](https://github.com/JarryShaw/PyPCAPKit/issues/522), [#542](https://github.com/JarryShaw/PyPCAPKit/pull/542)). - each crawler derives its `const/` path from the `pcapkit.vendor` root, so a crawler nested two levels down writes to the right file ([#741](https://github.com/JarryShaw/PyPCAPKit/pull/741)). +- `pcapkit.vendor.ipx` names its `sys.path` temporary `_path` rather than `path`, which read as a public name of the package, and the public-API test covers `pcapkit.vendor`, which it used to skip ([#1772](https://github.com/JarryShaw/PyPCAPKit/issues/1772), [#1779](https://github.com/JarryShaw/PyPCAPKit/pull/1779)). ### Project infrastructure @@ -381,10 +385,10 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r #### Changed - renames with no compatibility alias left behind: `examples/sample` and `examples/samples` -- one letter apart, holding different things -- are now `examples/captures` and `examples/generators`. -- the README is a landing page now, and Markdown rather than reStructuredText. `README.rst` (424 lines) became `README.md` (102), keeping what a reader arriving from PyPI or a search result needs (what the library is, why it exists rather than Scapy or DPKT, how to install it, a worked example, where the documentation lives). **Module Structure**, **Engine Comparison**, **Engine support by Python version**, **Test Environment**, **Test Results** and **Installation Notes** were already duplicated in fuller form in `docs/source/index.rst`, so they are linked rather than restated; the two blocks that existed nowhere else moved: **Testing** is now `docs/source/contributing/testing.rst`, and the `pipenv` and `make setup` block joined the Installation section of `docs/source/index.rst`. Requested by the project owner, and a deliberate exception to the reStructuredText convention, for the README only, since its renderers are GitHub and PyPI rather than Sphinx. `setup.py` reads `README.md` and declares `text/markdown`, and `MANIFEST.in` gains `include README.md`, which is belt-and-braces rather than load-bearing: setuptools' `sdist` ships a `README.md` regardless. Of the `include` lines only `CHANGELOG.md` is load-bearing, measured in the [#631](https://github.com/JarryShaw/PyPCAPKit/pull/631) entry below. `examples/benchmark/Dockerfile` copies `README.md`, and `examples/benchmark/benchmark.py` still says `README.rst` on purpose, meaning the benchmark suite's own README ([#619](https://github.com/JarryShaw/PyPCAPKit/pull/619)). +- the README is a landing page now, and Markdown rather than reStructuredText. `README.rst` (424 lines) became `README.md` (102), keeping what a reader arriving from PyPI or a search result needs (what the library is, why it exists rather than Scapy or DPKT, how to install it, a worked example, where the documentation lives). **Module Structure**, **Engine Comparison**, **Engine support by Python version**, **Test Environment**, **Test Results** and **Installation Notes** were already duplicated in fuller form in `docs/source/index.rst`, so they are linked rather than restated; the two blocks that existed nowhere else moved: **Testing** is now `docs/source/contributing/testing.rst`, and the `pipenv` and `make setup` block joined the Installation section of `docs/source/index.rst`. Requested by the project owner, and a deliberate exception to the reStructuredText convention, for the README only, since its renderers are GitHub and PyPI rather than Sphinx. `setup.py` reads `README.md` and declares `text/markdown`, and `MANIFEST.in` gains `include README.md`, which is belt-and-braces rather than load-bearing: setuptools' `sdist` ships a `README.md` regardless. Of the `include` lines only `CHANGELOG.md` is load-bearing, measured in the [#631](https://github.com/JarryShaw/PyPCAPKit/pull/631) entry below. `examples/benchmark/Dockerfile` copies `README.md`, while the `README.md` that `examples/benchmark/benchmark.py` names is the benchmark suite's own ([#619](https://github.com/JarryShaw/PyPCAPKit/pull/619)). - `CODE_OF_CONDUCT.md` moves from Contributor Covenant 1.4 to 3.0, at the maintainer's request. The text is the canonical 3.0 Markdown fetched from https://www.contributor-covenant.org/version/3/0/code_of_conduct/code_of_conduct.md rather than transcribed. Three things needed deciding. 3.0's two `[NOTE` placeholders are filled or removed: the reporting channel names `jarryshaw@icloud.com` (the contact 1.4 carried and `SECURITY.md` uses as email fallback) plus GitHub's report-abuse form for a report about the maintainer, which a single-maintainer project cannot otherwise cover; the enforcement placeholder is an instruction to the adopter and is removed. 3.0's plural "Community Moderators" become the singular maintainer (eight occurrences), this repository having no moderators. The four-rung ladder (Warning, Temporarily Limited Activities, Temporary Suspension, Permanent Ban) is a suggestion and is **kept**, each rung mapping to a lever one person holds on GitHub. 3.0 is CC BY-SA 4.0 where 1.4 carried no licence notice, so the attribution names version 3.0, links the permanent `version/3/0/` URL, carries the CC BY-SA 4.0 notice, indicates changes were made as BY requires, and says the share-alike term covers this document only (the code remains BSD-3-Clause, `LICENSE` untouched). The ladder renders as four list items each nesting three, checked against GitHub's Markdown API; [#613](https://github.com/JarryShaw/PyPCAPKit/pull/613) had to repair the 1.4 file when a stray list marker collapsed it into one nested item ([#624](https://github.com/JarryShaw/PyPCAPKit/pull/624)). - reconciled which private (`_xxx`) attributes and methods the Sphinx build documents, replacing an ad hoc mix with one rule: document the contract, hide the recipe. A member every subclass must implement, or whose shape a caller depends on, stays documented despite its underscore; a private helper that only keeps one method short does not. Seven module-level directives for pure implementation detail (`esp._resolve`, `esp._CRYPTO`, `ngap._convert`, `ngap._revert`, `ngap._PYCRATE`, `ngap._PDU_LOCK`, `pypcapfile._NamedStream`) were dropped, while 39 `autoattribute` directives were added for class-private state that *is* contract, among them `Extractor._flag_f`, the `PyPCAP` and `PCAP_CT` engines' `_backend` (the only two of the six third-party engines with one), `TraceFlow`'s internal fields, `FieldBase`/`Field` internals, the built-in `PCAP` engine's `_gbhdr`, `_vinfo`, `_nnsec` and `_dlink` (shared with `PyPCAP`, `PCAP_CT` and `PyPCAPFile`) and `PCAPNG`'s `_ctx`/`_ctx_list`. The 128 runtime definitions of `_missing_` (121 under `pcapkit.const`, 7 inline in `pcapkit.protocols`) gained one write-up in place of a directive per class, a new "Unrecognised Values" section in `docs/source/pcapkit/const/index.rst` cross-referenced from `registry.rst` (`conf.py` already excludes `_missing_` from every `autoclass`). `CONTRIBUTING.md` gained the rule as a named section. The one new Sphinx warning, a bare `Type` newly rendered by `TraceFlow._foutio`'s directive, is [#709](https://github.com/JarryShaw/PyPCAPKit/issues/709). No line under `pcapkit/` changed ([#684](https://github.com/JarryShaw/PyPCAPKit/issues/684)). -- `examples/captures/out.json`, `out.plist`, `out.txt` and `pcapng.txt` are no longer tracked in git; they are build output, not fixtures, and a tracked rendering with no reader goes stale silently. `pcapng.txt` is exactly that: it still recorded `packet -> NIL` for the four Enhanced Packet Blocks of `dhcp.pcapng` long after [#683](https://github.com/JarryShaw/PyPCAPKit/pull/683) gave those blocks their captured octets back. Rather than regenerate it again and leave the drift free to recur, the four files are removed from the index and folded into `examples/captures/`'s blanket `.gitignore` rule; the genuine inputs `in.pcap` and `dhcp.pcapng` stay tracked. `examples/legacy_smoke/Makefile` and its `README.rst` now describe regenerating these reports via `make fixtures`, and `tests/project/test_capture_tracking.py` pins the invariant ([#685](https://github.com/JarryShaw/PyPCAPKit/issues/685)). +- `examples/captures/out.json`, `out.plist`, `out.txt` and `pcapng.txt` are no longer tracked in git; they are build output, not fixtures, and a tracked rendering with no reader goes stale silently. `pcapng.txt` is exactly that: it still recorded `packet -> NIL` for the four Enhanced Packet Blocks of `dhcp.pcapng` long after [#683](https://github.com/JarryShaw/PyPCAPKit/pull/683) gave those blocks their captured octets back. Rather than regenerate it again and leave the drift free to recur, the four files are removed from the index and folded into `examples/captures/`'s blanket `.gitignore` rule; the genuine inputs `in.pcap` and `dhcp.pcapng` stay tracked. `examples/legacy_smoke/Makefile` and its `README.md` now describe regenerating these reports via `make fixtures`, and `tests/project/test_capture_tracking.py` pins the invariant ([#685](https://github.com/JarryShaw/PyPCAPKit/issues/685)). - `tests/project/test_isort_clean.py` gated on `isort` with a `try`/`except ImportError` inside `setUpClass`, which `tests/_dependency_gates.py` cannot see (`_gates_of()` walks a decorator list, never a function body), and `isort` was in no `pyproject.toml` extra, so the file reported `OK (skipped=1)` on every CI leg and the dependency-gate guard could not tell. It now gates on a module-level `HAS_ISORT` and a class-level `@unittest.skipUnless` so `gated_scopes()` counts it, `MODULE_PROVIDERS` maps `isort`, and `isort` joins the `test` extra rather than being excluded (`lint.yml` installs none, so unlike `mypy` there is no existing check to defer to). No workflow changed, every pytest job already building on `.[test]`; `gated_scopes()` goes 250 to 251 and `dependency_gate_gaps()` holds at 18. The third instance of the hazard [#745](https://github.com/JarryShaw/PyPCAPKit/issues/745) exists to stop ([#766](https://github.com/JarryShaw/PyPCAPKit/issues/766)). - `tests/vendor/test_vendor_reg_apptype_generator_unit.py` gated on `mypy` with no `MODULE_PROVIDERS` entry, so `_top_level_providers()` died on a bare `KeyError: 'mypy'` once the gate became visible -- a further instance of the hazard [#745](https://github.com/JarryShaw/PyPCAPKit/issues/745) exists to stop, like [#766](https://github.com/JarryShaw/PyPCAPKit/issues/766) above. `MODULE_PROVIDERS['mypy'] = ('mypy',)` fixes the lookup, and the three bare `MODULE_PROVIDERS[...]` subscripts route through that function, which raises an `AssertionError` naming the module, key and table on any future gap. `mypy` is deliberately not added to a `pyproject.toml` extra: no extra carries it, it is a `Pipfile` dev-dependency that `lint.yml` installs and already runs whole-package, and `lint.yml` runs no pytest job for `pytest_jobs()` to see, so the fix is a `DEPENDENCY_GATE_EXCLUSIONS['HAS_MYPY']` entry on `test`, `engine-tests` and `gate`. `dependency_gate_gaps()` goes 15 to 18 `(flag, job)` pairs, all three new and expected; ([#779](https://github.com/JarryShaw/PyPCAPKit/issues/779)). - `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 one 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)). @@ -393,6 +397,7 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r - 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; two `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 three 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)). +- the READMEs of `examples/benchmark` and `examples/legacy_smoke` are Markdown, `README.md` rather than `README.rst`, and `MANIFEST.in` ships them through `recursive-include examples *.md`. Stale build leftovers go: `setup.py`'s `bdist_wheel` override, since `build_py` and `install` already convert the tree the wheel archives, and `MANIFEST.in`'s `prune test`, which named no directory; `make profile` runs in `examples/legacy_smoke` ([#1773](https://github.com/JarryShaw/PyPCAPKit/issues/1773), [#1780](https://github.com/JarryShaw/PyPCAPKit/pull/1780)). #### Fixed @@ -411,6 +416,7 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r - `make_samples.py` builds fixtures from the tree it runs in, not the shared checkout ([#1343](https://github.com/JarryShaw/PyPCAPKit/issues/1343), [#1388](https://github.com/JarryShaw/PyPCAPKit/pull/1388)), and retries the Wireshark sample downloads on 429, 5xx and timeouts ([#1658](https://github.com/JarryShaw/PyPCAPKit/issues/1658), [#1659](https://github.com/JarryShaw/PyPCAPKit/pull/1659)). - the Conda Update workflow tags and uploads each bump exactly once ([#1632](https://github.com/JarryShaw/PyPCAPKit/issues/1632), [#1641](https://github.com/JarryShaw/PyPCAPKit/pull/1641)), stays green when a merge races its push ([#1649](https://github.com/JarryShaw/PyPCAPKit/issues/1649), [#1669](https://github.com/JarryShaw/PyPCAPKit/pull/1669)), and keeps the release's name and notes when it uploads ([#1657](https://github.com/JarryShaw/PyPCAPKit/issues/1657), [#1678](https://github.com/JarryShaw/PyPCAPKit/pull/1678)). Vendor Update commits the new modules a crawl adds ([#1685](https://github.com/JarryShaw/PyPCAPKit/issues/1685), [#1688](https://github.com/JarryShaw/PyPCAPKit/pull/1688)). - package `__all__` lists match what each package holds: six disagreed with it, and `from pcapkit.protocols.data import *` raised `AttributeError` on the unbound `HOPOPT_QuickStartOption` ([#515](https://github.com/JarryShaw/PyPCAPKit/issues/515), [#527](https://github.com/JarryShaw/PyPCAPKit/pull/527)); the public attributes several packages left out are listed ([#533](https://github.com/JarryShaw/PyPCAPKit/issues/533), [#544](https://github.com/JarryShaw/PyPCAPKit/pull/544)). +- the documentation build's 36 "more than one target found" warnings, fixed where they arise: an implicit reference to `dict`, `type` or `set` no longer matches a registry member such as `TCP.dict`, an enum member's docstring holding a URL no longer gains a spurious type field, and the hand-written `TypeVar` directives take the bare name under their `.. module::` ([#1775](https://github.com/JarryShaw/PyPCAPKit/issues/1775), [#1790](https://github.com/JarryShaw/PyPCAPKit/pull/1790)). --- diff --git a/docs/source/changelog/1.5.0.rst b/docs/source/changelog/1.5.0.rst index 7b963f05a..038afc553 100644 --- a/docs/source/changelog/1.5.0.rst +++ b/docs/source/changelog/1.5.0.rst @@ -4,8 +4,8 @@ The largest release since 1.0, and the first recorded here as it happened rather than reconstructed. Three more 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 669 issues and pull -requests from :pr:`340` to :pr:`1754`. +and a defect programme run through the issue tracker, citing 685 issues and pull +requests from :pr:`340` to :pr:`1791`. 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.0b12`` (2026-09-20 to 2026-10-13), dated in UTC and all @@ -152,7 +152,9 @@ Fixed per discarded default. The one deliberate divergence is kept and pinned: mutable registries look up, miss, and ``extend_enum`` rather than raising, so ``Method('FROBNICATE')``, ``ProtectionAuthority(1 << 70)`` and - ``AppType.get(65000, proto=tcp)`` still register. The three flag guards end in + ``AppType.get(65000, proto=tcp)`` still register; the :issue:`775` and + :issue:`860` entries below record how all three came to return an unregistered + member instead. The three flag guards end in ``return super()._missing_(value)``, so in-range composites still decompose and :issue:`623` stays fixed; ``Flags`` accepts ``0 <= value <= 0xFFFF``. A new ``tests/const/test_const_enum_builtin_parity.py`` asserts that every one of the 123 @@ -164,7 +166,8 @@ Fixed ``_missing_`` and minted regardless, so ``AppType.get(-1, proto='tcp')`` returned an unreachable ``PORT_-1_tcp`` while ``TCP(-1)`` raised. It now tests ``_missing_`` for ``None`` ("no row for this port") and lets the rejection - through; a valid but unassigned port still mints. ``Transport._make_port`` + through; a valid but unassigned port still minted, until :pr:`874` (below) made it + an unregistered member. ``Transport._make_port`` **passes an unvalidated** ``int``, **so** ``TCP.make(srcport=99999)`` **now raises where it minted junk.** Separately, the crawler rendered one ``_missing_`` branch per registry row, so the two spans IANA registers on more than one transport @@ -241,11 +244,11 @@ Fixed ``Dynamically Assigned Socket Numbers``, ``Statically Assigned Socket Numbers``, ``Dynamically Assigned``) convert, while ``Registered by Xerox`` keeps minting, a real ownership fact. 7 of the 89 are left alone: ``CGAType``'s ``Tag_`` mint is - not an IANA-style range, and the other 6 files sit on 9 classes not yet on - ``EnumRegistry`` (``AppType``, ``StatusCode``, ``ftp.return_code``, ``ftp.command``, - ``Method``, ``OptionType``), left to :pr:`859`/:issue:`860`. Each conversion - changes both the vendor crawler and the generated file, per :pr:`838`'s and - :pr:`858`'s precedent. The worked examples in + not an IANA-style range, and the other 6 files sit on the 9 classes :issue:`860` + moves onto ``EnumRegistry`` (``AppType``, ``StatusCode``, ``ftp.return_code``, + ``ftp.command``, ``Method``, ``OptionType``; :pr:`869` and :pr:`874`, below). + Each conversion changes both the vendor crawler and the generated file, per + :pr:`838`'s and :pr:`858`'s precedent. The worked examples in ``docs/source/contributing/conventions/mint-criterion.rst`` keep ``Registered by Xerox`` under Mint (:pr:`861`). * **a breaking change to** ``EtherType``'s ``_missing_`` ordering: values @@ -695,6 +698,11 @@ Fixed to nothing, or one never given, rather than a bare ``AttributeError``; the two registry modules share one ``NULL`` sentinel compared by identity, where each had its own string (:issue:`832`, :issue:`833`, :pr:`835`). +* ``PortEnumField`` given a ``proto`` with no ``.name`` -- a string such as + ``'tcp'``, or a composite such as ``TransportProtocol.tcp | TransportProtocol.udp``, + which is a bare ``int`` -- raised ``AttributeError`` on a port the registry does not + hold. The miss takes its transport from the registry's ``__transport__``, as + ``AppType.get`` does (:issue:`1774`, :pr:`1781`). pcapkit.dumpkit --------------- @@ -750,7 +758,10 @@ Fixed * **a breaking change to** the dump schema: the output writers dropped or corrupted what the packets carried. An ``OrderedMultiDict``, which every option list is, now dumps as a list of single-key entries in wire order - (:pr:`1305`). + (:pr:`1305`). Inside an object the writers fall back on, a class, module or + function is written as its qualified name rather than expanded, and an object met + again on its own expansion path as ```` (:issue:`1372`, + :pr:`1381`). * tree output raised on a ``complex`` value, and is now documented as lossy (:issue:`1266`, :pr:`1366`). * plist dates are written to whole seconds, so ``plistlib`` reads them back @@ -880,7 +891,7 @@ Changed ``__init_subclass__`` registration hook it adds, so the ``EngineBase[_P]`` that ``Extractor.engine`` returns leads to an API. Cross-references to ``Engine.run``, ``Engine.read_frame``, ``Engine.close`` and ``Engine.unsupported_reason`` now point - at ``EngineBase``. Exporting ``EngineBase`` in ``__all__`` is left to :issue:`1016` + at ``EngineBase``, which stays out of ``__all__``, as :issue:`1016` settled (:issue:`1024`). * **a breaking change to** ``register_mh_extension``: ``schema`` is keyword-only, as in the other fourteen schema-taking registrars (:issue:`516`, :pr:`526`). @@ -1286,6 +1297,16 @@ Changed is parsed generically, per :rfc:`6564`, rather than collapsing the packet to ``Raw`` (:issue:`891`, :pr:`904`). ``IPv6_Ext`` does it, and is also the shared base of all eight extension headers (:issue:`917`, :pr:`924`). +* **a breaking change to** ``pack()``: it no longer keeps the header it builds on the + instance it is called on, which in 1.4.1 too left ``schema`` disagreeing with + ``info``: ``UDP(srcport=1234).pack()`` reset ``schema.srcport`` to 0 while + ``info.srcport`` kept 1234. Once an instance has been parsed or constructed, + ``pack()`` builds on a shallow copy, so ``schema``, ``info`` and what ``make()`` + sets, such as TCP's ``connection``, stay as they were. The ``Frame``, ``PCAPNG`` + and ``HTTPv2`` overrides do the same, and re-packing a PCAP-NG block that carries + an option allowed only once no longer raises. Code that relied on ``pack()`` to + re-sync ``schema`` calls ``make()`` or constructs a new instance (:issue:`1769`, + :pr:`1791`). Fixed ~~~~~ @@ -1990,7 +2011,7 @@ Fixed ``PortEnumField`` and PCAP-NG's ``OptionEnumField`` minted a member through ``aenum.extend_enum`` for every value no row or documented span covers (111 calls on ``http.pcap``, all ephemeral ports). All four now peek at what ``.get()`` would - consult and mint only once the value is unresolvable; a genuine miss gets + 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: @@ -2276,7 +2297,9 @@ Fixed ``invalid header length`` message. Lengths 0 to 3 already raised ``ProtocolError``, so only the message changes (:issue:`1189`). * the RPL option, type ``0x23``, raised ``AttributeError`` in ``HOPOPT`` and - ``IPv6_Opts`` (:issue:`1196`, :pr:`1198`). + ``IPv6_Opts`` (:issue:`1196`, :pr:`1198`). The test that an option of any type in + the last two octets of the header fails in-library no longer skips it; test-only + (:issue:`1771`, :pr:`1776`). * **a breaking change to** the RPL option: it keeps its reserved flag bits on rebuild and reads :rfc:`6553` sub-TLVs (:issue:`1199`, :pr:`1201`). * ``make()`` rejected an enum member name given as ``str`` unless a namespace was @@ -2384,6 +2407,9 @@ Fixed unassigned value rather than minting ``Unassigned_N`` (:issue:`880`, :pr:`889`), and all four inherit ``get`` and its ``default`` from ``EnumLookup`` rather than overriding it (:issue:`935`, :pr:`940`). +* ``pcapkit.protocols.protocol.__all__`` lists ``Protocol`` beside ``ProtocolBase``, + so ``from pcapkit.protocols.protocol import *`` binds the module's public class + (:issue:`1770`, :pr:`1778`). pcapkit.toolkit --------------- @@ -2667,6 +2693,9 @@ Fixed piped stdin (:issue:`522`, :pr:`542`). * each crawler derives its ``const/`` path from the ``pcapkit.vendor`` root, so a crawler nested two levels down writes to the right file (:pr:`741`). +* ``pcapkit.vendor.ipx`` names its ``sys.path`` temporary ``_path`` rather than + ``path``, which read as a public name of the package, and the public-API test + covers ``pcapkit.vendor``, which it used to skip (:issue:`1772`, :pr:`1779`). Project infrastructure ---------------------- @@ -2738,9 +2767,9 @@ Changed gains ``include README.md``, which is belt-and-braces rather than load-bearing: setuptools' ``sdist`` ships a ``README.md`` regardless. Of the ``include`` lines only ``CHANGELOG.md`` is load-bearing, measured in the :pr:`631` entry below. - ``examples/benchmark/Dockerfile`` copies ``README.md``, and - ``examples/benchmark/benchmark.py`` still says ``README.rst`` on purpose, meaning the - benchmark suite's own README (:pr:`619`). + ``examples/benchmark/Dockerfile`` copies ``README.md``, while the ``README.md`` + that ``examples/benchmark/benchmark.py`` names is the benchmark suite's own + (:pr:`619`). * ``CODE_OF_CONDUCT.md`` moves from Contributor Covenant 1.4 to 3.0, at the maintainer's request. The text is the canonical 3.0 Markdown fetched from https://www.contributor-covenant.org/version/3/0/code_of_conduct/code_of_conduct.md @@ -2788,7 +2817,7 @@ Changed and leave the drift free to recur, the four files are removed from the index and folded into ``examples/captures/``'s blanket ``.gitignore`` rule; the genuine inputs ``in.pcap`` and ``dhcp.pcapng`` stay tracked. ``examples/legacy_smoke/Makefile`` and - its ``README.rst`` now describe regenerating these reports via ``make fixtures``, and + its ``README.md`` now describe regenerating these reports via ``make fixtures``, and ``tests/project/test_capture_tracking.py`` pins the invariant (:issue:`685`). * ``tests/project/test_isort_clean.py`` gated on ``isort`` with a ``try``/``except ImportError`` inside ``setUpClass``, which @@ -2876,6 +2905,13 @@ Changed * the ``Scapy`` and ``dev`` extras require ``scapy>=2.6.0`` (:issue:`1572`, :pr:`1578`), and the ``docs`` extra ``Sphinx>=7.3``, the first release with ``versionremoved`` (:pr:`1743`). +* the READMEs of ``examples/benchmark`` and ``examples/legacy_smoke`` are Markdown, + ``README.md`` rather than ``README.rst``, and ``MANIFEST.in`` ships them through + ``recursive-include examples *.md``. Stale build leftovers go: ``setup.py``'s + ``bdist_wheel`` override, since ``build_py`` and ``install`` already convert the + tree the wheel archives, and ``MANIFEST.in``'s ``prune test``, which named no + directory; ``make profile`` runs in ``examples/legacy_smoke`` (:issue:`1773`, + :pr:`1780`). Fixed ~~~~~ @@ -3066,3 +3102,9 @@ Fixed and ``from pcapkit.protocols.data import *`` raised ``AttributeError`` on the unbound ``HOPOPT_QuickStartOption`` (:issue:`515`, :pr:`527`); the public attributes several packages left out are listed (:issue:`533`, :pr:`544`). +* the documentation build's 36 "more than one target found" warnings, fixed where + they arise: an implicit reference to ``dict``, ``type`` or ``set`` no longer + matches a registry member such as ``TCP.dict``, an enum member's docstring holding + a URL no longer gains a spurious type field, and the hand-written ``TypeVar`` + directives take the bare name under their ``.. module::`` (:issue:`1775`, + :pr:`1790`). diff --git a/docs/source/contributing/conventions/process.rst b/docs/source/contributing/conventions/process.rst index c21c42e54..1d65f0ed5 100644 --- a/docs/source/contributing/conventions/process.rst +++ b/docs/source/contributing/conventions/process.rst @@ -101,7 +101,7 @@ commands down instead of a figure that will be stale by the next merge: The grouping scheme was settled on :issue:`918`: **a section per top-level module, with** ``Added``/``Changed``/``Fixed`` **nested inside each** -- module granularity, not per-file and not per-subpackage. The file carries **10** module-level sections holding -292 entries, and no entry carries an inline kind label:: +298 entries, and no entry carries an inline kind label:: $ grep -cE '^\* \*\*(Added|Changed|Fixed)\*\*' docs/source/changelog/1.5.0.rst 0