1.5.0 – 2026-10-29¶
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 to #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, #498).
pcapkit.const¶
Changed¶
a breaking change to
pcapkit.const.reg.apptype.AppTypebecomes a memberless base over one registry per transport protocol –TCP,UDP,SCTPandDCCP– becauseaenumrefuses to subclass an enumeration that has members.isinstance(TCP.http, AppType)still holds andAppType.get(port, proto=...)proxies to the registry, so nothing underpcapkit.protocolschanges. 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-registryMultiDictkeyed on port, so every service IANA registers on port 80 survives.get()answers with the canonical service, curated from/etc/servicesbecause IANA names no precedence, andget_all()reaches the aliases (#754).the
%-formattedraise ValueError(...)calls in the generatedpcapkit.const.reg.apptype.apptype, left by #759, are f-strings (#792). The remainder followed: the_missing_guards inpcapkit.vendor.tcp.flags,.ftp.commandand.http.method,AppType’s__new__/__repr__/__str__, and theextend_enumfallbacks inget()and in the span-handling tail. Theconsider-using-f-stringdisable drops fromtcp.flagsandreg.apptype.apptypeand stays onftp.commandandhttp.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).the
__repr__methods #798 left on%(inpcapkit/const/ftp/command.pyandpcapkit/const/http/method.py) are f-strings, so the disable drops from both files and bothpcapkit/vendor/templates. The other bespoke templates ({const,vendor}/ftp/return_code.py,{const,vendor}/http/status_code.py,vendor/http/frame.py) andvendor/default.pystill carry it, so #804’s claim that nothing inpcapkit/{const,vendor}/{ftp,http}/needs it holds for this pair only;repr()output is unchanged (#804).test_const_enum_no_mint.py’sis_manufactured()exemption for the hex-suffixed names #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 exactexempt_hits == 53bound stays. Test-only. Closes #879 (#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, andregister_apptypetakes its transports as varargs (#806, #815). Aliases show inrepr()andstr(), and can be registered per transport (#807, #837).a breaking change to
TransportProtocol: its base moves fromaenum.IntFlagtoaenum.IntEnum, so|between two members gives a plainintrather than a composite member (#808, #836).a breaking change to
TLSKeyLabel: the hand-written class inpcapkit.protocols.misc.pcapng.protocolbecomes the generatedpcapkit.const.pcapng.tls_key_label.TLSKeyLabel(PCAPNG_TLSKeyLabelinpcapkit.const.pcapng), crawled from the IANA registry RFC 9850 creates (#886, #890), and the old path is removed (#1744, #1754).a breaking change to
Method.get(): the method token is case-sensitive, per RFC 9110 Section 9.1, soMethod.get('get')no longer resolves toGET(#896, #907). It also looks a key up by value, soBASELINE-CONTROLandVERSION-CONTROLresolve (#908, #915).a breaking change to
ExtensionHeader: it is generated from IANA’s IPv6 Extension Header Types registry rather than the Protocol Numbers registry’s flag, and the two disagree on 147 (BIT-EMU) (#925, #926).the TCP and UDP
AppTyperegistries load on first use rather than at import (#1538, #1560).a breaking change to
LLACode:Wilcardis spelledWildcardandMHisMN, the names RFC 5568 gives codes 0 and 2, with no alias for either old name (#1848, #1872).a breaking change to
tcp.flags.Flags: each member is its wire mask in the flags field,FIN0x001up toAE0x100, rather than1 << offsetin IANA’s most-significant-first bit numbering, which putFINat0x8000.int(tcp.connection)of a SYN/ACK is0x012, as the header carries it, not0x4800, andFlags(0x12)isSYN | ACK. The range guard narrows to the 12-bit flags field,0 <= value <= 0xFFF. Code that stored or compared the old integers has to change; membership tests and names are unaffected (#2310).
Fixed¶
constant lookups that rejected a value the registry defines.
RouterAlert(0)is the only value RFC 2113 defines and the one IGMP, RSVP and MLD actually send, and it was discarded because the vendor crawler skipped a header row IANA’s CSV does not have; IPXSocket(0)is that protocol’s own default, sobytes(IPX(...))crashed on its own defaults; and the FTP_missing_overrides were plain methods rather than classmethods, so every unregistered value raisedTypeErrorinstead of extending the enumeration (#492, #503).the string-keyed
get()inpcapkit.const.ftp.commandandpcapkit.const.http.methodtested membership with the raw key but registeredkey.upper(), so the first lowercase or mixed-case token raisedTypeError: 'RETR' already in use. Reachable from wire data:pcapkit.protocols.application.ftp.protocolmatches withre.Iand passes the match verbatim, and RFC 959 Section 5 makes FTP commands case-insensitive, soretr file.txtwas 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 duplicateGETwould have lacked the real one’ssafeandidempotentattributes (#582, #583).http.methodno longer folds case: RFC 9110 Section 9.1 makes the method token case-sensitive, soMethod.get()matches it exactly (#896, above).get()’s documenteddefaultwas ignored on the integer path across the generatedpcapkit.consttree, because_missing_cannot see the caller’sdefault–Hardware.get(99999, 0)raisedValueErrorinstead of returning0. The integer path now consultsdefaultbefore letting the error escape.-1, the placeholder the generated signature already carried, still separates “no default” from “a default”; #859 replaced it with an identity sentinel once the shared base #858 (below) putget()behind one call site. The sweep covers the integer registries generated from the shared template, not only those #584 named;pcapngOptionTypeandregAppTypecarry 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)._missing_in the Mobility Header flag enumerations (BindingACKFlag,BindingUpdateFlag,HandoverACKFlag,HandoverInitiateFlag) ended inreturn cls(value), the constructor that had just failed, so every in-range value that is not already a member re-entered_missing_unbounded and raisedRecursionError–F(0), no flags set, and every composite such asBindingACKFlag(0x06). Defining_missing_at all caused it, by shadowing theaenumFlagmachinery that resolves exactly those values;pcapkit/const/tcp/flags.pyhad none and so never had the defect (it has one now, from #647 below, with the same tail). Each now ends inreturn super()._missing_(value), aspcapkit/vendor/default.pyemits and most const modules (#647’s new ones included) already did, soF(0)is an empty flag andBindingACKFlag(0x06)isS|D. Minting throughextend_enumwas rejected for a flag type: naming0x06Unassigned_0x06would hide the composite and add a member per bit pattern. The range guard is untouched (#623).constant registries rejected an invalid value differently from the built-in
enum, or did not reject it at all.Flags,ftp.command.CommandTypeandreg.apptype.TransportProtocolareIntFlagtypes with no_missing_, soaenumcomposed a pseudo-member for any integer:Flags(-1)returned65520(every TCP flag set, from a value no 16-bit field can hold) andFlags(-65536)none.ftp.command.Command,FEATCodeandhttp.method.MethodareStrEnumtypes whose_missing_calledvalue.upper()before checking the type, so an integer raisedAttributeError. These now raise a bareValueError, asenum.IntEnumdoes and the other modules already did. #647’s proposal to replace those withpcapkit.utilities.exceptions.EnumErrorwas deliberately rejected:EnumErroris(BaseError, TypeError), not aValueError, so it would diverge from the built-in, walk past theexcept ValueErrorin every generatedget()body (silently undoing #584), and log CRITICAL fromBaseError.__init__once per discarded default. The one deliberate divergence is kept and pinned: mutable registries look up, miss, andextend_enumrather than raising, soMethod('FROBNICATE'),ProtectionAuthority(1 << 70)andAppType.get(65000, proto=tcp)still register; the #775 and #860 entries below record how each came to return an unregistered member instead. The flag guards end inreturn super()._missing_(value), so in-range composites still decompose and #623 stays fixed;Flagsaccepts0 <= value <= 0xFFFF. A newtests/const/test_const_enum_builtin_parity.pyasserts that every registry raises the same exception type as a stdlibenum.IntEnum; it keys onaenum.Enumbecause anIntFlagis not anIntEnumsubclass, which is how theIntFlagones escaped the existing sweeps (#647).a breaking change to
AppType.get(): an out-of-range port is refused rather than minted. Itsexcept ValueErrorcaught the range rejection from_missing_and minted regardless, soAppType.get(-1, proto='tcp')returned an unreachablePORT_-1_tcpwhileTCP(-1)raised. It now tests_missing_forNone(“no row for this port”) and lets the rejection through; a valid but unassigned port still minted, until #874 (below) made it an unregistered member.Transport._make_port()passes an unvalidatedint, soTCP.make(srcport=99999)now raises where it minted junk; in 1.4.1,TCP(srcport=N)raisedAttributeErrorfor any bareint. Separately, the crawler rendered one_missing_branch per registry row, so the 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 whoseprotoreadtcp,6666/udpansweredircurather thanreserved, andproto='sctp'mintedx11where IANA assigns nothing. A branch naming a transport now testscls.__transport__; the branches naming none are unchanged (#764).a breaking change to
AppType._dispatch(): it resolved aprotonaming several transports by its lowest set bit, andtcpis lowest, so every composite containing it dispatched into the TCP registry – silently, and undetectably to a caller checking equality, sinceAppType.__eq__compares onportalone.TransportProtocolwas still anaenum.IntFlagand a member’sprotocarried the whole set IANA assigned the service, sotcp | udpwas an ordinary value to pass back in; theAppType.protoandTransportProtocolentries above record how #806 and #808 later retired both. Over every multi-transport member this misrouted port 888 (accessbuilderresolved to TCP’scddbp) and port 999 (puprouterresolved to TCP’sgarcon). It now raisesProtocolError, aValueErrorsubclass, so the documented contract holds. No library caller passes a composite (#759).get()’s string-key miss and every generated registry’s_missing_bounded-range branch calledaenum.extend_enum()on an unrecognised value, so e.g.Hardware(40)minted a permanent member. Fixed at the shared sites inpcapkit/vendor/default.py, plus a newregister(value, name)base classmethod for the caller-named path that still mints. Every generated registry is updated; the non-mintingget()miss applies to all of them, but the_missing_fix changes behaviour only for those with a bounded-rangeextend_enumbranch.pcapkit/toolkit/scapy.pyandpcapkit/toolkit/pyshark.pyfedget()a string key with no default and now raiseMissingKeyErrorinstead of receiving a placeholder; the reader sites inhopopt.py/ipv6_opts.pyconvert theKeyErrorintoProtocolError. Riding along:pcapkit/toolkit/pyshark.pygains aFILTER_NAME_TO_LINKTYPEmap (ethtoETHERNET,trtoIEEE802_5), consulted ahead ofLinkType.getsince pyshark reports a filter name, not a DLT name. The other minting call sites in_missing_,getand the hand-writtenmh.pyandngap.pywere deferred; theregister/register_aliassites are not deferred but the by-design caller-named path (#838).a breaking change to
pcapkit.const.ipx.socket.Socket’s_missing_: archived range branches that 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 generatorpcapkit/vendor/ipx/socket.pynow lists each subset range ahead of the range containing it. 36891 of the 65521 values that_missing_resolves (56.3%) change name: 32 fromRegistered by XeroxtoExperimental, 4095 fromDynamically AssignedtoDynamically Assigned Socket Numbersand 32764 fromDynamically AssignedtoStatically Assigned Socket Numbers. No defined member changes and every value still resolves (#847).LinkType(209).namereturned'IPMB_LINUX', which tcpdump’s own table marks “Legacy names (do not use)”, because a 2024-05-04 regeneration emitted that row above the currentI2C_LINUX = 209andaenumgives a value to the member defined first.pcapkit/vendor/reg/linktype.pysinks 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_LINUXstays a reachable alias (LinkType['IPMB_LINUX']still resolves to 209). Not breaking: only the canonical name for the shared value moves (#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, #854).a breaking change to more const registries’
_missing_: applies the owner’s mint/unmint ruling (settled on #847, confirmed on #775: a final, concrete assigned name mints, and a notation for readers does not) across the_missing_files still callingextend_enum. Most convert wholly, 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. Some are mixed:EtherType’sDEC Unassignedand “Old Xerox Experimental values. Invalid as an Ethertype since 1983.” branches convert, while vendor-company names (Xyplex, Motorola, Walker Richer & Quinn and others) keep minting, since a proprietary protocol has no public name beyond the company’s;IEEE802_3_Length_FieldandBerkeley_Trailer_encap_IPalso 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, whileRegistered by Xeroxkeeps minting, a real ownership fact. Left alone areCGAType, whoseTag_<hex>mint is not an IANA-style range, and the files holding the classes #860 moves ontoEnumRegistry(theAppType,StatusCode,ftp.return_code,ftp.command,MethodandOptionTypemodules; #869 and #874, below). Each conversion changes both the vendor crawler and the generated file, per #838’s and #858’s precedent. The worked examples indocs/source/contributing/conventions/mint-criterion.rstkeepRegistered by Xeroxunder Mint (#861).a breaking change to
EtherType’s_missing_ordering: values0x0101-0x01FFnow resolve to their Old Xerox Experimental name instead of anIEEE802_3_Length_Field_0x0101-style one. The generated method tested0x0000 <= value <= 0x05DC(IEEE802.3 Length Field) first, and the Old Xerox range sits wholly inside it, so that branch was unreachable. Unlike #841’s IPX fix, these bounds come from the live IANA CSV, so the fix is general:pcapkit/vendor/reg/ethertype.py’sprocess()collects range rows and a newEtherType._insert_rangeplaces 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 CSV’s ranges. Closes #862 (#865).a breaking change to
Method:__new__calledstr.__new__(cls)with no argument, so every registered member’sstrpayload was empty regardless of value –str(Method.GET) == ''andMethod.GET == 'GET'wasFalse;.valuewas always correct. #869 (above) had fixed the unregistered path only, leavingstr(Method('frob')) == 'frob'besidestr(Method.GET) == ''. Fixed tostr.__new__(cls, value), mirroringCommand.__new__, in the generator (pcapkit/vendor/http/method.py) and the generated module. No sibling hand-rolled__new__has the defect (OptionType/AppTypedeliberately store a display string). Consequences, all corrections:Method.GET == 'GET'isTrue;bool(Method.GET)flipsFalsetoTrue;json.dumps(Method.GET)emits"GET"not"";sorted()is lexicographic rather than input-order-stable; and{m: ... for m in Method}gains an entry per member where it had one, because every member previously hashed as''and compared equal. Nothing underpcapkit/protocolsrelied on the old behaviour. Closes #870 (#871).a breaking change to
TransportProtocolandAppType: step 2 of #860, theAppTypehalf (the other bespoke registries were #869, above).AppTypeand its transport subclasses move ontoEnumRegistryand both mint sites –_missing_’s range branches andget()’s own mint (PORT_{port}_{transport}->'unknown') – now build through a new_unregistered_memberoverride that reconstructssvc/port/proto, per the owner’s ruling that only IANA-registered values are legitimate and thatget()lacks the information to create new ones. Every branch converts, including those naming a real service assigned to a whole port span (x11,active-net,satvid-datalnk,vrml-multi-use,ircu,swx,flex-lm); unlikeFEATCode’s fix (#869, above) there is no import-time self-mutation to cure by declaring members statically, so these stay deliberately unregistered lookups.AppTypegains a workingregister(); the base’s would have built a member withsvc='<null>'. The breaking part:TransportProtocolmoves from power-of-two values (tcp=1, udp=2, sctp=4, dccp=8) to sequentialauto()(sctp=3, dccp=4), per the owner’s follow-up ruling once|-joined composites stopped being parsed.AppType.get()treats whateverintit is given as a whole, so the renumbering changes what specific integers mean: onAppType.get(80, proto=...), a bare4resolved toSCTP.httponmainand now silently resolves toDCCP.unknown, because a real member sits at4under both numberings; a bare8used to mintDCCP.PORT_80_dccpand now raisesValueError. The composed-value case is deliberately untested, per the owner’s ruling that a breaking change already retired that path (#874).closes the remaining scope of #775:
EtherTypeandSocket, the last registries whose_missing_minted a permanent member, now return a non-registeringcls._unregistered_member(value, name). The issue’s AST census ofextend_enumin_missing_/getis down to the singleCGATypecarve-out (its mint is not an IANA-style range).EtherType(0x0888)used to add a member to__members__and return the identical object on a repeat; it now returns<EtherType.Xyplex_0x0888: 2184>with__members__unchanged, absent from_value2member_map_, equal but not identical across lookups – likewiseSocket(0x0010). The regeneratedEtherTypewas not checked against the live IANA CSV, only against the committed fixture. Keeping each branch’s hex-suffixed name reintroduces the value-derived shapetest_const_enum_no_mint.py’sis_manufactured()sweep forbids; rather than weaken the predicate, a narrow exemption covers exactly these files, since the collision the check guards against cannot occur once_unregistered_membernever registers (#878).OptionType.register()andCommand.register()built members thatget()could not find again, andOptionType.register_alias()raisednot a registered OptionTypefor every argument. Both registries now round-trip throughget(), in the generated files and the vendor templates (#1311, #1330, #1369).the Quick-Start function registries rejected 9 to 15, though RFC 4782 gives the Function field four bits; the whole 4-bit range now resolves (#1350, #1353).
every three-digit FTP reply and HTTP status code resolves, including those no registry row names (#1443, #1444).
the
AppTyperegistry’s dict union is defined only on interpreters whosedicthas one (#1694, #1703).NGAP’s
ProcedureCodeandProtocolIEare generated intopcapkit.const.ngapfrompycrate’s compiled NGAP specification, with no network fetch. An unseen value gets a non-registering member, where every unknown key used to mint a member at the shared value-1and so aliased every other (#880, #899).FEATCode.get()matches case-insensitively, as RFC 5797 Section 2 defines FEAT codes; the registry-wide case-sensitivity audit found no other registry that needed to fold (#903, #913).registerrefuses whatgetrefuses.TCP.register(70000, 'x')andHardware.register(70000, 'x')minted a member thatgetthen returned, because no registry overrode the_validate_valuehookregistercalls. Each generated registry,AppTypeincluded, now overrides it with the check its_missing_makes, raisingEnumValueError.get(value, default)on a value the hook refuses still returnsdefaultand logs nothing (#1792, #1818, #1816, #1823; the quiet miss is #1845, #1873, underpcapkit.corekit).AppType.register()andOptionType.register()keep that rejection loud inside a quietget, asEnumRegistry.register()does (#1916, #1927).Command.get()refuses a key that is not astrwithEnumValueError, asMethod.get()does.get(b'\x01')minted a member keyed bybytes, andget(-1)raisedAttributeErrorwhatever itsdefault(#1824, #1826).IANA’s port spans that name no transport answer
AppType.get()with the asking registry’s transport rather thanTransportProtocol.undefined, as itsunknownfallback for a port in no span already did:TCP.get(8012)isunassigned [tcp]. A port IANA lists on other transports only staysunknown: 8011 isreserved [tcp]andbier-echo [udp], andunknown [dccp]andunknown [sctp]. Lookups still mint nothing (#860) and an out-of-range port still raises (#2048, #2052).Method._missing_resolves an HTTP method whose registered name differs from its own value, soMethod('baseline-control')andMethod('version-control')giveBASELINE_CONTROLandVERSION_CONTROLrather than minting an unregistered member: the lookup falls back to the value map on the upper-cased value before it mints.Method.get()stays case-sensitive (#2284, #2351).
pcapkit.corekit¶
Added¶
tests/corekit/test_fields_numbers_width_repair.py, covering each octet boundary in its own method, since the defect is a pattern and a single case would pass against a fix that special-cased the reported width (#599).FieldBaseShortReadPaddingSideTests, covering both byte orders at 2, 4 and 8 octets and every truncation point, including the figures #604 reports, plus guard rails for a full read and for a read against an empty buffer, the #431 behaviour the option and list loops depend on (#604).EnumLookup, the base carryinggetandget_allthatEnumRegistryand the non-registry enumerations share (#877, #906, #921, #932). Itsgetraises pcapkit exceptions in the shape of the stdlibEnum: a name miss raisesEnumKeyError, aKeyError(#923, #927).MultiInfoandOrderedMultiInfo, immutableInfosubclasses that are also aMultiDictand anOrderedMultiDict, so a repeated key keeps every value. The data models’ option lists use them (#1484, #1523).pcapkit.corekit.fields.byteorder, holdingpacket_byteorderandbyteorder_callback, the byte-order helpers the PCAP and PCAP-NG schemas share (#1528, #1711, #1715).can_seek(), whether a stream really seeks: on Windows a pipe answersseekable()withTruewhile itsseekdoes nothing. It trustsseekable()only for a regular file or a stream with no descriptor.Extractorwraps any other stream inSeekableReader, sopcapkit -reads a Windows pipe (#2097, #2104), andadvance()skips a record on one by reading (#2108, #2118,93a6173ae).
Changed¶
Probe,CipherSuiteandIntegritySuiteareInfosubclasses rather thantyping.NamedTuple. They are Mappings now, solen()and iteration yield field names rather than values.TCPSegmentinpcapkit.toolkit.pcapstays atyping.NamedTuple, since it is built for every TCP frame and anInfocosts more to construct.ModuleDescriptor.klassreads an already-imported module out ofsys.modulesrather than re-enteringimportlib.import_module, since next layer dispatch resolves a descriptor there on a per-frame path. Neither a registry hit holding aModuleDescriptornor a miss writes the resolved class back: a memoised hit went stale onimportlib.reload(#1559), and recording a miss in a class-levelcollections.defaultdictis the defect #421/#426 fixed at this layer, #425/#428 for the option, chunk and block registries, and #555/#560 at the schema layer. A resolution now costs ~117 ns rather than ~436 ns, which is not measurable inextract()wall clock. The change is taken for its shape:sys.modulesstays the only module cache, whereas a memo of the class would serve the pre-reload class after animportlib.reloadforever and failisinstanceagainst the live one. Proposed by@Ts-Boomin #563; the implementation differs because that one added a second, never-invalidated cache of resolved classes (#574).SeekableReader.truncate()raises instead of returning a size, and the misspelledwriteable()is nowwritable(). Both break external callers; nothing inside the package calls either.io.IOBasedocuments one gate over two methods – “If False, write() and truncate() will raise OSError” – and this reader’swritable()answers False, yettruncatereturned its new size, whereopen(path, 'rb').truncate()raises. The refusal isUnsupportedOperation('truncate'), the same onewriteraises; pcapkit’s subclassesio.UnsupportedOperation. The old resizing is kept as the private_truncate_buffer: it resizes a private lookback window, not the stream, and it is the only route to the “position sits before the window” state thatseekand the buffered read paths must refuse, for which #643 and #644 landed tests.writeable()was never an override of anything, soio,shutiland third parties never saw it, and the value reported is unchanged (#645).a breaking change to
@final, now enforced at runtime onInfoandSchema.info_finalandschema_finalboth endreturn final(cls), so every finalised class carriedtyping.final’s__final__marker and nothing read it.@finalalone on anInfoorSchemasubclass – without@info_finalor@schema_final– now raisesInfoErrororSchemaErrorat first construction, the class being marked final but never finalised and so carrying no generated__init__.__init_subclass__cannot catch that, sincefinalapplies after class creation, so the check sits in the existing one-shotFinalisedState.NONEbranch and a finalised class pays nothing. Deriving from a finalised class raises too, from newInfo.__init_subclass__()andSchema.__init_subclass__()hooks (before, onlyEnumSchemahad one).SchemaErroris aValueError, so code catchingTypeErroraround a subclass declaration will not see it.@info_final @finaland@final @info_finalboth finalise silently; the re-entry check keys on__finalised__rather than__final__, because only the former records the decorator having run.@info_finaltwice warns and returns the class unchanged.pcapkit.utilities.compattakesfinalfromtyping_extensionsbelow 3.11 rather than 3.8, becausetyping.finalonly records__final__from 3.11. The guard cannot fire on the library’s own classes (#778).a breaking change to
Info.to_dict(): it returns anOrderedMultiDictat every level rather than adict, so a key that repeats, such as a second IPv6 extension header of one type, survives afrom_dict(to_dict())rebuild.type(d) is dictno longer holds (#1453, #1466, #1484, #1523).a breaking change to
pcapkit.utilities.decorators: it moves topcapkit.corekit.decorators, with no re-export.seekset,beholderandprepareare no longer importable frompcapkit.utilities, and the module logs aspcapkit.corekit.decorators(#1735, #1737).a breaking change to the sentinels: all live in
pcapkit.corekit.sentinels, which exports the objectsNULL,NO_VALUEandNO_DEFAULTand their common baseSentinel, but not their types, andNoValueis spelledNO_VALUE(#916, #911, #922, #937, #939). No moved name keeps a re-export, soNoValueTypehas only that home, not its 1.4.x one inpcapkit.corekit.fields.field(#1744, #1754).
Fixed¶
stdlib exceptions leaking out where the library’s own were promised: a malformed IP field value raised a bare
ValueErrorinstead ofFieldValueError(#465); abooladdress was silently packed as0.0.0.1or0.0.0.0,boolbeing anintsubclass (#491, #500); and@preparediscarded extra arguments silently and treated a declared zero length as end of stream, which is now distinguished from a genuinely exhausted one and raisesStreamEOFError(#454, #458).FieldBase.unpack()zero-padded up to a field’s declaredlengthwithrjust()however little databufferheld; a ~40-octet PCAP-NG Decryption Secrets Block with a bogus inner length forced a multi-gigabyte allocation, sincelengthis often wire-derived. A declared length past 262144 octets – libpcap’sMAXIMUM_SNAPLENand this package’s defaultsnaplen– that the buffer cannot back now raisesFieldValueError; the option and list loops’ tolerance for a short read past a truncated area (#431) is far under that ceiling and untouched (#554).that ceiling bounds one field, and a packet holds many, so the sum of a parse’s zero padding was still unbounded: 200 minimal PCAP-NG Decryption Secrets Blocks (4,800 wire octets, each declaring
secrets_length262,142 against two supplied octets) retained 50.0 MiB, 10,922x per block, with the per-field guard never firing.FieldBase.unpack()now keeps a per-context ledger of octets supplied against octets synthesised, and raisesFieldValueErroronce the total of shortfalls past 65,536 octets passes262144 + 16 * supplied. The same 200 blocks retain 0.2 MiB (54.6x). A shortfall of 65,536 octets or fewer, the span of a 16-bit wire length, is padded unconditionally and charged to nothing, so no snapshot-truncated capture is subject to the budget; without that band the same legitimate frame parsed differently depending on what had been parsed before. What this does not close: a length declared by a 16-bit field can still be repeated without limit, and at this layer a crafted file is indistinguishable from a legitimate snapshot-truncated capture of offload-sized segments; separating them needs the frame’s ownincl_len/orig_len. Nothing resets the ledger, so it bounds everything a context has parsed rather than one file (#573).which exception a malformed TCP SACK option raised depended on unrelated process state: a clean interpreter raised
ProtocolErroras documented, but a process that had poppedpcapkit.corekit.fields.miscfromsys.modules(as the #439 ABC-cache regression tests do in everysetUp/tearDown) raisedFieldValueErrorfrom a different layer, before the documented check (#525).ListField.unpack()resolvedSchemaFieldthrough a function-local import re-run on every call; a module reimported mid-process is a second, distinct class, soisinstancemisclassified the field and billed each item by its declared length instead of what it consumed. Any caller relying on the previously-observedFieldValueErrornow getsProtocolError, deterministically, matching the docstring.a
NumberFieldwhoselengthwas a callable could not pack or parse at any widthstructhas a native integer code for.lengthis-1until the callable resolves,-1has no native code, and the template builder raised_need_processfor it and never cleared it – a latch. Resolving the real width rebuilt the template but left the latch set, sopre_processhandedbytesto a template that had become>Q(struct.error: required argument is not an integer), and parsing failed in mirror by callingint.from_byteson an integerstruct.unpackhad produced. The flag is now recomputed from the width in force, which also keeps a callable resolving to a width with no native code (3 octets) byte-packed. Every native width (1, 2, 4 and 8 octets) was affected, not only the 8 reported, onNumberFieldandEnumFieldalike. This made every extended 8-octet MPTCP DSS form unbuildable, those widths being chosen at runtime from the DSS flags; #585 worked around it in the TCP schema alone (#591).NumberField.pre_process()sized a value with floor division dressed as a ceiling. When a field is packed whilelengthis still the-1placeholder, the width came frommath.ceil(value.bit_length() // 8); the//had already floored, somath.ceildid nothing and the width was one octet short:256at one octet,65536at two,16777216at three. The reach is wider than “just past a boundary”: every bit length that is not a multiple of eight is wrong, so1(bit length 1, floored to zero octets) and every value from 1 to 127 failed too. It is now the ceiling it was meant to be. The repair is reached only by packing a field the caller never resolved (a schema resolves every field first), which is why #591’s suite, whose values all had byte-aligned bit lengths, missed it; #591’s fix neither caused nor masked it. Deliberately left alone: a signed field is sized without room for its sign bit, so an unresolved signed field still cannot pack128. The same typo in the ILNP nonce option builders is a separate change (#599).FieldBase.unpack()padded a short read on the wrong side. When a buffer falls short of the declared length – the accommodation that lets a snapshot-truncated capture parse (#431) – the shortfall was zero-filled withrjust(), placing the zeros at the front, as if the octets never read were the leading ones; a short read has lost the trailing ones. It is nowljust(), correct for both byte orders: one octet of a four-octet little-endian120read as2013265920, and three octets of a four-octet big-endian0x01020304as0x10203. A full read is untouched (#604).SeekableReader.truncate()put its padding where the reader’s bookkeeping says the content is, andreadthen returned one octet fewer than the buffer held. Same family as #604, but separate defects. The buffer keeps content at[0:_buffer_cur]with padding behind it, so a reduction must keep the octets it has and an extension must append at the tail;temp[-size:]sliced the buffer rather than the content, keeping trailing padding and discarding octets read, andtemp.rjust(size)prefixed the zeros. Separately,readcapped atmin(size, self._buffer_cur - 1), one short at the start and reaching into padding further in, and made up the shortfall from the stream past the skipped octet. The reported symptom needed both:read(4),truncate(8),seek(0),read(8)overb'abcde'returnedb'\x00\x00\x00e'and now returnsb'abcde'. Further contract faults are fixed with it: the position was reset to the buffer start rather than left alone; an omittedsizeresized to0rather than the current position; and_buffer_curwas left addressing octets a reduced buffer no longer had, so the next read raisedValueError: memoryview assignment: lvalue and rvalue have different structuresfrom_write_buffer.truncate(0)raised the same by a second route (buf[-self._buffer_size:]isbuf[-0:], the whole buffer), so_write_buffernow counts from the front; that state is reachable only throughtruncate. A reduction also advances_buffer_setpast the dropped octets, keeping_buffer_set + _buffer_curequal to how far the stream has been consumed, whichseekreads as its licence to fetch more; clamping_buffer_curalone made that sum under-report, and the next forwardseeksilently spliced in octets from the wrong offset (read(8),truncate(3),seek(6),read(1)overb'abcdefghijklmnop'returnedb'l'whereb'g'sits at offset 6). Latent rather than live: nothing here callstruncate, though it is public (#622).the 16-bit band the #573 entry above records as deliberately left open: a length declared by a 16-bit wire field could still be repeated without limit. The crafted PCAP-NG of 2,000 Enhanced Packet Blocks, each with an option declaring 65,535 octets against four real ones, synthesised 1,637x its size in zeros and now synthesises none, with every frame and option still parsed. The bound is taken one layer up, where the missing information exists: a PCAP-NG block’s Block Total Length is authoritative and cross-checked against its trailing copy, so the option area is that length less the fixed fields,
captured_lenand its padding, and an option declaring more payload than the area has left is malformed. A snapshot-truncated capture says so throughcaptured_lenand leaves its options whole, so it never trips this – which the #571len(buffer) < lengthrejection could not distinguish, and why it was declined. A newbounded_optionclamps the payload to the octets the area has left and warns withSchemaWarning; it is applied to every variable-width option and record payload in the PCAP-NG schema, and deliberately not to the block-level payload fields, the 32-bit band the running ledger already budgets. A companionbounded_areaclamps a packet block’s option area to the octets the block itself holds less the trailing Block Total Length: the area was otherwise sized from a declared length nothing checks against the file, so a block declaring 1,000,000 octets while holding 36 synthesised 65,535 zeros with no warning. Both are no-ops on a well-formed block. The non-packet blocks’ option areas keep the framing assumption; the general fix for a declared length reaching a read is #678. Clamping rather than refusing keeps the #431 accommodation, since a PCAP-NG block read has no catch point aboveFieldBase.unpack()and one refusal would abort a whole extraction rather than one block; and the clamp reads only the block’s own declared framing, never a running total, so identical input answers identically whatever preceded it. The skip for a remainder already past zero is load-bearing on the unpacking path, where clamping would give the field a negative template such as'-2s'. No clamp fires on the sample captures, and #678’s uncaughtValueErroron truncated input is unchanged (#594).a breaking change to
FieldBase.length: it calledstruct.calcsizeon a template built from a negative resolved length (e.g.'-5s', from alength=callback whose running counter had been overdrawn), raising a barestruct.errorthat ordinary caller code does not catch. The chokepoint every affected schema shares now raisesProtocolError, coveringapplication/{ftp,httpv1,httpv2,ngap}.py,internet/{hip,ipv6_route,mh}.py,link/ethernet.py,misc/pcapng.pyandtransport/sctp.pywith no other file changed; a non-negative length is unaffected. This closes the follow-on thehttpv2length-guard entry underpcapkit.protocolsfiled as out of scope (#805).a breaking change to
EnumRegistry.register(): it now raisesValueErrornaming the existing member and pointing atregister_alias(), where it used to silently alias an already-registered value under the caller’s new name –aenumtreats a taken value as an alias request, soregister(existing_value, 'TOTALLY_NEW_NAME')returned the existing member, minted nothing and raised nothing, contradicting its docstring.pcapkit.corekit.enum.EnumRegistryis a new mixin implementingget,get_all,register,register_alias,register_aliasesand_unregistered_memberonce, per the owner’s ruling on #842; it first converts registries with no bespoke__new__(ipv6.extension_header.ExtensionHeader,tcp.flags.Flagsand themh.*_flagmodules), and servesIntEnum,IntFlagandStrEnumalike. The guard reaches only those: the other const registries still carry the old generator’s bareextend_enum(cls, name, value), sopcapkit.constships contradictoryregistercontracts until #858 (below). These also gain the generated template’sKeyErrorcatching on a missing name, soFlags.get('NOPE', 2048)returnsFlags.ACKrather than raising; and a key that is neitherintnorstr, which raisedTypeErroron theIntFlagregistries (Flags.get(3.5)), now raisesValueError, tried as a value. OnExtensionHeaderit raisedKeyError, the shift #858’s entry attributes to the other registries (#855).a breaking change to
EnumRegistry.register(), extended: the const registries still on the generated template’s bareextend_enumnow inherit the guard #855 (above) added, soTransType.register(6, 'TOTALLY_NEW_NAME')raisesValueErrornaming the value and the member that holds it and pointing atregister_alias(), where it used to mint and raise nothing.pcapkit/vendor/default.py’s template now emitsclass {NAME}(EnumRegistry, IntEnum)and drops its ownget/register/_unregistered_member. The modules with a bespoke__new__(ftp/command,ftp/return_code,http/method,http/status_code,pcapng/option_typeandreg/apptypeamong them) are left alone, so every other class inheritsEnumRegistry(#855).get()’s dispatch shifts only for a key that is neitherintnorstr(KeyError, tried as a name; nowValueError, tried as a value), a key type nothing in the library passes.pcapkit/corekit/enums.pyis renamedenum.py, the owner’s ruling (#858).EnumRegistry.get()’sstr-key branch tried only a name lookup (cls._member_map_[key]) and on a miss raised or returnedcls(default), never the value path, so on aStrEnumregistry a key that resolved through the constructor (_Str('known-value')) raisedKeyErrorthroughget(), and a supplieddefaultoverrode a good value match the code never attempted. It now falls back to a plain_value2member_map_lookup once the name lookup misses and before consultingdefault. Deliberately notcls(key):FEATCode._missing_()(pcapkit/const/ftp/command.py) mints viaextend_enumfor any unrecognised string, so routing through the constructor would let a failed name lookup mint a permanent member once a registry like it converts; a dict lookup never reaches_missing_. A name still wins when a string is both a name and a different member’s value. Step 1 of #860; step 2, converting the bespoke registries, follows (#869, #874, below). The non-strpath is unchanged. Widens rather than breaks: a call that raisedKeyErrornow resolves, and no caller that got a correct result gets a different one (#863).a breaking change to
EnumRegistry.get(): its docstring claimed “It never mints”, but eachcls(default)call site reached_missing_for adefaultinside a still-minting registry’s range, growing the registry as a side effect of resolvingdefaultrather thankey. Per the owner’s ruling that onlyregister()mints, andget()only through the ranges_missing_mints, each site now resolvesdefaultthrough a plain_value2member_map_lookup;keyresolution is unchanged, soEtherType.get(0x0888)still mintsXyplex_0x0888through its own_missing_, as #775’s deliberate exception permits. Adefaultnaming no registered member now falls through to the errorkeyitself would have raised, rather than a fresh error about the default – the accepted cost of the ruling. The docstring’s “It never mints” is qualified todefaultonly. SoHardware.get('Definitely-Not-A-Member', 40), with40inHardware’s declared-but-unassigned range, now raisesKeyErrornaming the original key. Closes #864 (#868).ProtoChain.__add__()returned the left operand’s cachedprotocolsandaliases: oncea.protocolshad been read,a + ProtoChain(IPv4)reportedchain == 'Ethernet:IPv4'butprotocols == (Ethernet,).copy.copy(self)carried the cache across; the result is now built fresh, asfrom_listbuilds one, and+=follows, since it falls back to__add__(#1099).a
SchemaFieldwith no declared length handed its-1placeholder to the nested schema as that schema’s length, so every item of such a list warnedpacket length < 0: a HIPLOCATOR_SETand an MH CGA Parameters item among them.SchemaField.unpack()now passes what is left in the buffer as a declared length – notNone, which would turn a truncated item’sFieldValueErrorintoprepare()’s quietStreamEOFError. Fixed-length items are unchanged; the HTTP/2SETTINGSpair, whose size is fixed, is given it in its own schema instead (#1140, below) (#1148).a signed
NumberFielddecoded as unsigned, a zero-lengthPayloadFieldmisread, and an emptyRawdid not rebuild (#1331).from_dict(to_dict())lost nested values, andlen()of anInfocounted its hidden keys (#1334).StringFieldpacks back exactly the octets it unpacked, where a guessed charset, aU+FFFDsubstitution or a character-count length used to change them; the decoded text is unchanged (#1293, #1336). A charset that rejects'replace'falls back to UTF-8 (#1706, #1728), and Python 3.8 unquotes throughpcapkit.utilities.compat.unquote(#1693, #1707).IPv4InterfaceFieldkeeps a mask that is not a netmask, and a deep copy of anOrderedMultiDictkeeps its order (#1346).a breaking change to padding: non-zero padding octets are kept on rebuild rather than zeroed (#1223, #1436).
fields keep the bits and octets they captured on rebuild: a
BitField’s unnamed bits (#1487, #1495), aNumberField’s bits outsidebit_length(#1488, #1497), and the bytes an untypedListFieldunpacked (#1489, #1494). An unnamedListField,PayloadFieldorSchemaFieldgets a default name (#1491, #1493).each non-final
Infosubclass is finalised on its own state (#1490, #1496), and its constructor reads only the class’s own annotations (#1692, #1702).copying or pickling a stream proxy no longer recurses (#1524, #1544).
@preparereadslengthandpacketby keyword as well as by position, soSchema.unpack(data)andSchema.unpack(data, length=2)work where they raisedIndexError(#444, #450).ListFieldunpacks a schema item from the per-item copy its callback andlength_callbackconfigured, not from the unconfigured original (#433, #453).an
*IntFieldgiven asigned=that contradicts its fixed sign, such asUInt8Field(signed=True), raisesFieldValueError; the argument used to be discarded silently (#545, #549).negative lengths raise
ProtocolErrorrather than a bare stdlib error:FieldBase.lengthtells a malformed template from a negative resolved length (#825, #827), andNumberFieldchecks a negative resolvedlengthand a negativebit_length(#828, #829, #831, #834).ModuleDescriptor.klassraisesProtocolErrorfor a class name that resolves to nothing, or one never given, rather than a bareAttributeError; the registry modules share oneNULLsentinel compared by identity, where each had its own string (#832, #833, #835).PortEnumFieldgiven aprotowith no.name– a string such as'tcp', or a composite such asTransportProtocol.tcp | TransportProtocol.udp, which is a bareint– raisedAttributeErroron a port the registry does not hold. The miss takes its transport from the registry’s__transport__, asAppType.get()does (#1774, #1781).EnumLookup.get()answers a value miss quietly, whetherdefaultanswers it or the miss is raised: a registry’s_validate_valuerejection logs nothing and installs no exception hook there, whileEnumRegistry.register()still reports the same rejection loudly (#1845, #1873).each sentinel is a singleton, as
Noneis: calling its type,copy,deepcopyandpickleat every protocol give back the one object, andNO_VALUEreprs as its own name rather than as an object address. Their new common base,Sentinel, is public; a sentinel’s type cannot be subclassed, and neither it nor the sentinel takes an attribute write. A pickle written before this change at protocol 0 or 1 still loads, though not as the singleton (#1841, #1842, #1876).defects off CPython on Linux. On GraalPy,
SeekableReader’s reads aliased a buffer a later slide rewrote, so they copy out of it, and aMultiDictnested each value list, so it is built from adict(#1963, #1974); and a sentinel type refused the cache writes_py_abcmakes, soisinstanceagainst it raisedTypeError(#1960, #1966).Field.packhandsstructabytearrayasbytesfor thes,pandcformats, which PyPy refuses forsandpand GraalPy accepts forcwhere CPython does not, so it packs alike on every interpreter (#2004, #2007, #2015, #2018). On Windows,SeekableReader(buffer_save=True)with nobuffer_pathcould not reopen its temporary file; it creates one it can, and deletes it onclose()or collection (#2098, #2105).PortEnumFieldandSwitchFieldare exported frompcapkit.corekit,pcapkit.corekit.fieldsandpcapkit.all, andSchemaField,nested_packet_contextandBytesFieldfrom the modules defining them, soimport *binds them (#2174, #2191).a
ListFieldcallback is handed the per-item copy it configures, not the shared field, and aSchemaFieldwith a callable length sizes its template from the resolved length rather than1024s. No parse in the library reached either (#2173, #2190).PortEnumFieldrange-checks the port it is handed, so a directly built TCP, UDP or SCTP schema raisesFieldValueErrorfor a port outside the field’s width, wheresrcport=70000was masked to0x1170and-1to0xffff.NumberFieldkeeps masking, which is its documented contract; theNumberFieldandSchemadocstrings now say that direct construction range-checks nothing, and name where the checks live (#2314, #2335).
pcapkit.dumpkit¶
Fixed¶
a flag value with no declared bits no longer dumps as
Type::None [0]. This changes user-visible output in every textual dump format.make_dumper’sobject_hookrenders each enumeration member asType::name [value]and interpolated the name unguarded, but aFlagvalue composed entirely of undeclared bits hasname is None, so the literalNonelanded in the name half:Flags(0)rendered asFlags::None [0]injson,tree,text,txt,plistandxml, out of bothExtractorandTraceFlow. A nameless member now renders with the value’s decimal spelling, soFlags(0)givesFlags::0 [0]andFlags(8)givesFlags::8 [8]. That is the spelling the enumeration libraries use for an undeclared residue (Flags(2057).nameisACK|9), and it cannot be mistaken for a member name, since an identifier may not begin with a digit, whereasNONEis a real declared name elsewhere. The guard is onname is Noneand not on the value, because the defect was never about zero (Flags(1)andFlags(9)are equally nameless). Surfaced by #634, which made a flagless TCP segment reach this path (#648).tests/dumpkit/test_nameless_enum_rendering_unit.py(added by #670) carried probes assuming every flag-enum registry is 16 bits wide:test_scalar_return_renders_a_nameless_member_as_its_valueused one fixed tuple ending in65536, andtest_no_flag_registry_renders_the_literal_noneswept every registry but only againstregistry(0). Widths differ:ftp.command.CommandType3 bits,reg.apptype.TransportProtocol4 (computed at runtime), themhack/initiate flags 8, andmh.binding_update_flag.BindingUpdateFlagandtcp.flags.Flags16.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).the plist writer emitted mapping keys unescaped, so a key carrying
&,<or>produced a report that is not well-formed XML.dictdumper’s_append_dictinterpolates the key into'<key>{item}</key>'and calls_encode_valueon the value only, so both branches ofpcapkit.dumpkit.commonthat build a mapping now escape their keys through oneescape_keyhelper – aMultiDict, where #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-strkey is escaped too: a PCAP-NG decryption secrets block keys TLS key log entries by a raw bytes client random whose repr can carry any of those characters. The upstream half isJarryShaw/DictDumper#125; this does not wait on it. The json writer’s quoting defect stands, upstream asJarryShaw/DictDumper#121(#772).the JSON writer emitted mapping keys unescaped, so the report of
test.pcapngdid not parse (Expecting ':' delimiter): its TLS key log is keyed by abytesclient random whose repr holds"and\.escape_keyinpcapkit.dumpkit.commonnow JSON-escapes keys for JSON output, as #772 made it XML-escape them for plist, and the JSON report of everyexamples/captures/file now parses. The upstream defect,JarryShaw/DictDumper#121, stands (#1152). The PCAP-NG end-to-end test of that report no longer skips; test-only (#1795, #1809).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). 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<circular reference: T>(#1372, #1381).tree output raised on a
complexvalue, and is now documented as lossy (#1266, #1366).plist dates are written to whole seconds, so
plistlibreads them back (#1448, #1450).a PCAP trace record scales its fraction to the trace’s resolution (#1500, #1529), and writes the octets it has rather than a truncated record’s claimed length (#1541, #1543).
a subclass of a dumper
make_dumperbuilds recursed without end in itsobject_hookfallback, which calledsuper(type(self), self)(#2144, #2146).
pcapkit.foundation¶
Added¶
the
engine='pypcap'andengine='pcap_ct'extraction engines, independent distributions of the samelibpcapinterface, andengine='pypcapfile'(#386, #405). They buy speed by doing less – neitherpypcapnorpcap_ctdissects at all, so they offer no reassembly or flow tracing, andpypcapfilehas no IPv6 decoder. Install only one ofpypcapandpcap-ct: both own the top-levelpcapmodule, and with both presentpcap-ctwins the import and the other becomes unselectable. The missing interface constantsPyPCAP,PCAP_CTandPyPCAPFileare now exported alongsideDPKT,Scapy,PySharkandPCAPKit(#412). Python 3.11 is the last interpreter on which every built-in engine can run, and even therepypcapandpcap-ctcannot coexist.EngineBase.unsupported_reason(), a preflight every engine answers andExtractor.run()consults before anything is imported. An engine that cannot run in the current environment now gives one warning naming the real cause (a Python version, a missingtsharkorlibpcap, the wrongpcapdistribution) and a clean fall back topcapkit’s own parser, rather than an error from inside the third-party package (#396, #405). The same holds for an engine whose package is not installed at all (#1045).conflicton the reassembly data models: absolute, inclusive ranges where two fragments claimed the same span with different bytes, previously lost silently on both the IP (#482) and TCP (#443, #478) paths.the declarations that pair a
__proto__dispatch registry with the enumeration its codes come from, replacing the module-level tables that held the pairing apart from the__proto__they describe.__proto_key__, on each class owning such a registry (Link,Internet,Frame,PCAPNG,TCP,UDPandSCTP), names that enumeration and whether a bareintis also valid (#2392);__proto_dest__, on theLinkType,EtherType,TransTypeandPayloadProtocolIdentifierregistries, names the destination each enumeration keys, which is howcode=<member>infers one without an explicit destination. Both are rendered attributes, and the vendor crawlers generate the latter from a newPROTO_DEST(#2403).
Changed¶
layer=andprotocol=are honoured rather than inert. Both were read under the wrong names, so every value a caller passed was dropped into**kwargs; the CLI’s-Lalso now validates its argument. The packet context reaches the schema layer for the first time, so a field the wire elides can be resolved from its enclosing packet (#404).follow_tcp_streamdispatches on the engine type, where both branches of the old test were dead and the native adapter ran against every engine’s frames (#402).reassembly and flow-tracing defaults moved (#435), visible to a caller.
Datagram.completedwidened fromboolto aCompletionenumeration (COMPLETE,PARTIAL,TIMEOUT); onlyCOMPLETEis truthy, soif datagram.completed:is unaffected butdatagram.completed == Trueno longer holds. TCP flow tracing is bidirectional by default, which merges each flow’s two halves and closes one only once both have FINed – 331 flows become 111 on the sample HTTP capture, the difference being single-frame stray tails; passtrace_bidirectional=Falsefor the old behaviour. IP reassembly also gained the 60-second timeout [RFC 1122, RFC 8200], clocked off the capture’s own timestamps, tunable withreasm_timeout=; TCP reassembly gets no timeout by default.trace_analyse=is new, and reassembles each traced flow’s application layer.conflicting TCP overlaps resolve first-write-wins, per RFC 9293 section 3.10, where they had silently resolved last-write-wins (#443, #478). A deliberate, narrow behaviour break: a conforming retransmission carries identical bytes. IP fragment reassembly keeps last-write-wins, because RFC 791 specifies the opposite resolution, and records the disagreement instead (#482).
renames with no compatibility alias left behind:
HoleDiscriptoris spelledHoleDescriptorand its package aliasTCP_HoleDiscriptorisTCP_HoleDescriptor(#350).subclass registration is opt-in for
Engine,Reassembly,TraceFlowanddumpkit’sDumper(#514). Each registers if and only if its registry keyword is given –engine=forEngine,protocol=forReassemblyandTraceFlow,fmt=forDumper. Previously an absent keyword fell back to the class’ own name, so every subclass of the public class was registered, and declining meant subclassing the parallel*Baseclass under an alias, which every built-in does (hence no public class had a subclass, while every built-in derives from its*Baseclass). This breaks out-of-tree code that subclasses one of these classes and relies on the derived key; pass the keyword, or call the matchingregister_*function. Nothing the library ships is affected, and the*Baseclasses remain importable. Silent failures are now loud: an unrecognised class keyword raisesUnsupportedCallinstead of being swallowed by**kwargs(passingname=to aReassemblysubclass silently ignored the key, sinceprotocol=is the real one), and so doesDumper’sext=withoutfmt=. A class attribute is not an opt-in:__engine_name__and__protocol_name__still set the name a class reports, registered or not. Each metaclass gained a class-levelregistryproperty mirroringEnumSchema.registry, and aDumpersubclass no longer touches the filesystem while itsclassstatement runs.Engine’s keyword isengine=rather than thename=first shipped, becausemcls,name,basesandnamespacecollide withabc.ABCMeta.__new__’s parameters on Python 3.10 and raiseTypeErrorbefore the hook runs. There is noname=alias: a keyword that works on some interpreters and not others is the trap being removed.a breaking change to
Extractor.register_engine(),register_reassemblyandregister_traceflow, and theregister_extractor_*wrappers over them: each now accepts only a subclass of the publicEngine,ReassemblyorTraceFlow(#1016). They had accepted a subclass of the matching*Baseclass since #513, which was a tolerance for pcapkit’s own built-ins (every built-in derives from the base, none from the public class) rather than a contract:*Baseis documented as internal, and the public class is the one carrying the registration hook. Third-party code that subclassesEngineBase,ReassemblyBaseorTraceFlowBasedirectly and registers through these functions now getsRegistryErrornaming the public class; subclass the public class instead. The error messages are unchanged. The built-ins never pass through a registrar;Extractor._register_internal_engineand its sibling internal registrars check the base for internal registration, and are what the #513 regression test now exercises.register_protocolis untouched (#1020).extraction is around 46% faster on a 1,117-frame HTTP capture, with byte-identical output (#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). Flow tracing over the same capture went from 1416.6 ms to 744.0 ms, because the flow dumper handed each record to a
Frameconstructor that re-dissected the whole stack to return the bytes it had just been given; options are no longer parsed twice either (#427).a breaking change to
Extractor.engineandExtractor.record_header(): both are now declared to returnEngineBase[_P]rather thanEngine, and the private_exengattribute follows. The built-in engines subclassEngineBasedirectly, so none was ever anEngine, which adds only the__init_subclass__registration hook. A type checker sees the change: code passing the result to anEngine-typed parameter must now acceptEngineBase; and because the old declarations were unparameterised,engine.read_frame()andrecord_header().read_frame()used to beAnyand are now the extractor’s own frame type, so an assignment that relied onAnythere no longer type-checks (#1022).the documentation of the engine base classes: the member stubs (
name,module,registry,extractor,unsupported_reason,run,read_frame,closeand the rest) moved fromEnginetoEngineBase, where they are defined, andEnginenow documents only the__init_subclass__registration hook it adds, so theEngineBase[_P]thatExtractor.enginereturns leads to an API. Cross-references toEngine.run,Engine.read_frame,Engine.closeandEngine.unsupported_reasonnow point atEngineBase, which stays out of__all__, as #1016 settled (#1024).a breaking change to
register_mh_extension:schemais keyword-only, as in the other schema-taking registrars (#516, #526).the
pypcapandpcap_ctengines probe thepcapbackend once per extraction rather than three times, and find its distribution without parsing each candidate’sMETADATA, whichimportlib.metadatadoes on Python 3.15 and which madepcap_ctextraction about 2.4x slower there (#2110, #2112, #2113, #2121).
Fixed¶
TCP reassembly mixed absolute sequence numbers with buffer-relative slicing, so on any capture carrying a SYN with a realistic initial sequence number incomplete datagrams were dropped silently, and the
completed=Falsebranch of the public API was unreachable (#349, #376).format='text'raisedAttributeErrorbefore writing anything, naming adictdumper.Textthat has never existed. It now points atTree, as the'txt'alias beside it already did.Extractorhad the ownership of its input stream inverted: a handle it opened itself, fromfingiven as a path, was never closed, while a stream the caller supplied and still needed was._cleanupclosed undernot self._flag_s(the flag that gated theopen()), and theSeekableReaderwrapping a non-seekable input asked forstream_closing=not self._flag_sin the same wrong direction. The leak was one descriptor per extraction, which surfaced as aResourceWarningfailing the test suite (#577, #596, #600; #606 only made the test immune). Both conditions now follow ownership via a singleExtractor._owns_input, and a finaliser releases the handle of an extraction abandoned before end of file (auto=False, iterated part way and dropped); the files #610 names emit no such warning. Leaving awithblock follows the same rule, so a caller’s stream stays open after it (#1999, #2000). The finaliser is aweakref.finalize, so it also runs where__del__never does, as on GraalPy (#1997, #1998).extract(..., no_eof=True)never returned. End of stream was detected andExtractionWarning: EOF reachedfired, but every loop that handles it (record_frames,__next__,__call__) readif self._flag_n: continue, so the flag suppressed the error that ended the loop without supplying another ending and an exhausted input was retried forever. The flag is not pointless –__main__setsno_eofforfin='-', and the end of what has arrived on a live capture is not the end of the capture – so this is a termination condition rather than a deletion. The input’s own position tells a paused capture from a finished one:prepareraises end of stream when zero bytes remain and restores the position first, so two consecutive ends of stream at the same position mean nothing arrived between them (Extractor._note_eof_progress). A pipe is unaffected, since a read there blocks while the writer is open but idle. This also fixespcapkit -, which hung on any finished stdin. One deliberate narrowing: a seekable input does not block, so a file still being appended to ends at the data present when the extraction reached it; a timed grace period would make the cut-off intermittent rather than absent, so following a growing file is left to a policy of its own. Pre-existing and untouched: an append landing mid-record raisesValueError: read length must be non-negative or -1. The end-to-end tests are bounded by a child process rather thantests._support.time_limit, because the in-process deadline proved intermittent on this loop, and an intermittent guard against a hang leaves a wedged suite rather than a red test. Guesses at the cause are ruled out: the parse path’sexcept Exceptionhandlers are never entered during the spin, and the memory is the retry loop’sEOF reachedrecords, which the runner retains (#620).register_protocoldisplaced one protocol class with another and said nothing. The registry behindpcapkit.protocols.__proto__is keyed oncls.__name__.upper(), and several dispatchable classes are namedHTTP(the basepcapkit.protocols.application.http.protocol.HTTPand thehttpv1andhttpv2implementations), so a bare assignment decided the winner:register_protocol(httpv2.HTTP)moved__proto__['HTTP']to thehttpv2class with no exception, log line orRegistryWarning, andProtocolBase.expand_comp('HTTP')then resolved to whichever registered last. The overwrite now raises aRegistryWarningnaming the displaced and the replacing class. The sharpest path is not a call to the registrar:Protocol.__init_subclass__()registers unconditionally, so merely defining a subclass of the publicProtocolunder a name a built-in holds displaced that built-in. A plainimport pcapkitemits zero new warnings: the built-inHTTPclasses derive fromProtocolBase, so__init_subclass__never fires for them, and import-time seeding keys off the distinct namesHTTP,HTTPv1andHTTPv2. The guard deliberately reads “key present and the incumbent is a different class”, not mere presence (#726 later gave its sibling registrars the same identity guard): this function is the funnel every wrapper registrar ends in, so registering one class under two codes (register_tcpthenregister_udp, a documented path) reaches it twice with nothing displaced. The collision is reported, not resolved: re-keying is a registry-format change that none of the bare-name readers can absorb (each degrades silently to a string orRawon a miss), so it belongs to the registry redesign in #514. The issue called this the one registrar with no warning; in fact none warned by itself, the siblings only by delegating to a guarded classmethod (#675).the
#:autodoc comments inpcapkit/foundation/traceflow/traceflow.pythat named a bareType, which Sphinx resolves against every class namedTypein the project rather thantyping.Type, and silently linked topcapkit.const.l2tp.type.Type, an L2TP field-type enum; once #684 renderedTraceFlow._foutio, the built docs pointed at the wrong class with no warning. These sites now spell it~typing.Type[Dumper], among the sites #709 lists. The other bareTypesites, in hand-written:type:fields underdocs/source/pcapkit/foundation/, are fixed separately by #714 (#709).register_protocol’s overwrite warning could claim a protocol was replaced with itself. The guard was correct (incumbent is not protocol, an identity check from #681), but the message built both operands with a barerepr(). A factory defining a same-named closure-local class on every call (astests/protocols/test_construction_keyword_check_unit.py’s_protocol_classdoes) produces two distinct classes sharing one__module__and__qualname__, so a real overwrite read “overwriting X with X.” The message now compares the two reprs and, only when they coincide, appends each object’sid();__module__/__qualname__would not disambiguate, being what the repr already renders (#710).more registrars warned on mere key presence rather than an actual overwrite, outside the wording of #718’s identity guard (
incumbent is not None and incumbent is not new, landed by #726), whose issue named only code-keyed registrars:register_engine,register_reassemblyandregister_traceflowonExtractor, andregister_dumperon bothExtractorandTraceFlow. Each now compares the incumbent by identity before warning; bothregister_dumpersites compare only the stored dumper, so re-registering with just a new file extension stays silent too. Non-breaking: a correct caller sees strictly fewer warnings (#739).the class registrars listed below now raise
RegistryErrorfor a non-class argument, where a bareTypeError(“issubclass() arg 1 must be a class”) used to escape – from the guard itself for theregister_dumpermethods, and from insideabcfor the others. They areregister_dumper,register_engine,register_reassemblyandregister_traceflowonExtractor,TraceFlow.register_dumper()(reached throughregister_traceflow_dumper), andpcapkit.foundation.registry.protocols.register_protocol. Each guard gains an explicitisinstance(x, type)test ahead of itsissubclass; in those that accept aModuleDescriptorit runs after the descriptor is unwrapped, so a descriptor naming a non-class attribute is rejected too, whileregister_protocoltakes no descriptor. No guard’s target class changes, and a wrong class still raisesRegistryErroras before.RegistryErrorsubclassesTypeError, soexcept TypeErrorstill catches it; a caller matching the exact type, or the old message, does not. The same guard in the protocolregisterclassmethods is fixed separately by #1026 (#1021).a missing extraction engine package now gives a single
EngineWarningrather than a duplicate (#1045, #1046).Extractor.import_test()warned that the package was absent, thenExtractor.run()warned again when it fell back to the default engine;runnow stays silent there, andimport_testsays whatrunused to:engine <name> (<module>) is not installed; using default engine instead, the module name in backticks. Only code that matchedimport_test’s old wording, or that expects the duplicate warning, breaks.Extractordefects:Extractor.output’sUnsupportedCallnamedformatrather thanoutput;make_namelooked an unknown format up in the__output__defaultdictand so inserted it; and the Scapy engine never set_offmt, soExtractor.formatraisedAttributeErrorunderengine='scapy'(#1095).the DPKT, Scapy and PyShark engines call
Extractor.record_header()at setup again, as they did until 2023, so the global-header step is shared with the default engine and sets_offmtfor each of them. Their dumped output changes: JSON, plist and tree reports gain the leadingGlobal Headerrecord (Section Header 1for PCAP-NG) the default engine already writes, identical to its, andfiles=Truewrites that record’s file too; frame records are unchanged.Extractor.formatnow also answers on a capture with no frames.format='pcap'failing on every engine is a separate defect, #1127 (below) (#1104).format='pcap'failed on every engine with aTypeErrorfromPCAPIO.__init__(), missingprotocol:Extractor.__init__()created the writer before any global header was read, soPCAPIOhad no link type. The header step now creates it throughExtractor._open_output(), andrun()opens it for engines with no such step; a PCAP writer takes the frame itself and skips theGlobal Headerrecord, so the default engine writesin.pcapback byte for byte. The third-party engines hand the writer no frame octets, so they fall back to JSON with aFormatWarning, as their flow tracing already does, and a PCAP-NG input raisesFormatError(#1127). The written global header also dropped the input’sthiszone,sigfigsandsnaplen, so most sample PCAP files did not come back byte for byte; the PCAP engine now passes those fields through_open_outputtoPCAPIO, and every one does (#1165).IP reassembly reports only the octets it holds: a truncated fragment, a strict run and a zero-length datagram no longer report octets from the declared Total Length (#1368). Only the final fragment’s partial block counts, and a datagram too long for its length field never completes (#1582, #1585, #1598).
TCP reassembly judges completion from each buffer’s own gaps, compares sequence numbers modulo
2**32, and holds a FIN until reordered data arrives (#1375). A TSO fragment’s length is its captured length, andRCVBTcovers the last block (#1564).a second TCP flow with the same label truncated the first flow’s file; a reused label now gets the next free suffix (#1360, #1371).
the engine, reassembly, traceflow and dumper registrars accept a built-in back under the name it shipped with, restoring the shipped entry after an override; the engine, reassembly and traceflow registrars still refuse any other built-in (#1363, #1376).
PCAP-NG blocks are numbered across the whole file, not per section (#1256, #1426).
format='pcap'is kept when an unavailable engine falls back to the default one (#1264, #1430).extract()accepts a file object with nonameasfin(#1506, #1509), and reads every frame from a pipe or other non-seekable stream (#1586, #1595).the third-party engines read what the capture holds. The dpkt engine keeps a frame dpkt cannot parse rather than leaking
AttributeError(#1351, #1377), applies each PCAP-NG interface’s link type and timestamp resolution rather than the first one’s (#1379, #1396), and reads float timestamps, Simple Packet Blocks and raw-IP frames (#1550). The scapy engine resets its interfaces at each PCAP-NG section (#1522, #1546), takes a packet’s link type and clock from its interface (#1563), and reads each record whole (#1717, #1733). The pyshark engine skips non-packet PCAP-NG blocks (#1515, #1531), and the pypcapfile engine reads the PCAP records pypcapfile drops (#1512, #1569). dpkt and scapy write real PCAP traces (#1507, #1511).a callable passed as
verbose=is called on every engine. The scapy, dpkt, pyshark, pypcap, pcap-ct and pypcapfile engines replaced it with their own printer, which now runs only forverbose=True, its output unchanged (#1874, #1880).an
Extractorwhose constructor raises closes what it owns, the input it opened and then the engine it chose, so a PyShark capture’stsharkdoes not outlive it; the original exception still propagates, and a caller’s stream stays open (#2100, #2106, #2107, #2119).engine='pypcap'warns and falls back to the default engine when thepcapmodule is installed but cannot load, asengine='pcap_ct'already did, where it raised the loader’sOSError(#2132, #2133).TCP reassembly bounded its holes with
sys.maxsize, which on GraalPy and 32-bit builds is2**31 - 1, inside TCP’s sequence space, so a stream past it never completed. The bound isHOLE_OPEN_BOUND,2**63 - 1, the value 64-bit builds already used (#1959, #1965).the method-name registrars, such as
register_ipv4_option, andregister_linktype,register_pcap,register_pcapng,register_ethertypeandregister_transtyperefuse acodethat is not an enum member withRegistryErrorbefore touching a registry, where they registered it and then raisedAttributeError, leaving the entry behind (#2186, #2193).pcapkit.foundation.registry.protocols.register_protocol_code, and thecode=class keyword, check every destination’s key before registering any of them. A key belonging to another enum, such as{Frame: TransType.ICMP}, is refused withRegistryErrorwhere it replaced the shippedLinkType.ETHERNETentry, and a bad target late in the list no longer leaves the earlier ones registered. A destination keeping a registry of its own, such asLoopbackandIPX, is not checked against another enum (#2268, #2343).the number registrars
register_tcp,register_udp,register_sctpandregister_apptyperefuse aboolcode withRegistryError, whereregister_tcp(True, ...)registered port 1, sinceboolis anintsubclass (#2269, #2344).IP reassembly reports a datagram that is not itself fragmented but whose BUFID is already buffered: it submits that buffer as incomplete and then reassembles the datagram in a buffer of its own, per RFC 791 Section 3.2, where the datagram was never reported at all. On IPv6 an atomic fragment is reassembled in a buffer of its own and submitted without flushing the datagram already being reassembled, so that one’s final fragment still completes it, per RFC 6946 Section 4 (#2270, #2345).
the PyPCAP, pcap-ct and pypcapfile engines warn with
AttributeWarning, naming the engine, that they ignore a parsing context supplied throughExtractor’scontext=, as the dpkt, scapy and pyshark engines already did, where they dropped it silently (#2313, #2332).Deferredcould not be pickled under protocol 0 or 1 – a slotted class with no__getstate__raisesTypeErrorthere, independent of theanalyze/protoregression #2417 fixed. It now defines__getstate__and__setstate__, mirroring the(dict_or_None, slots)pair Python’s own default slotted-class reduction already used for every released build, so aDeferredpickled by one of those still unpickles. The slot names come fromcopyreg._slotnamesrather than a hand-rolled walk oftype(self).__mro__, which would otherwise shadow a base class’s slots under a subclass declaring its own, miscount a bare-string__slots__ = 'name', or read a private, name-mangled slot under its unmangled spelling (#2423, #2425, #2424).that same
Deferredstill failed to pickle under protocol 0 or 1 on GraalPy 25.4, after #2423’s fix:copyreg._reduce_ex, which handles protocol 0 and 1 before__getstate__is ever consulted, walkscls.__mro__for a base to compute a base-object state from, and on GraalPy that walk stops atGenericitself rather than atobject, raisingTypeError: typing.Generic() takes no arguments.Deferred.__reduce_ex__now hard-codes thebase=object, state=Noneoutcome the walk always reached on CPython for protocol 0 and 1, so the walk – andGeneric– is no longer part of the route pickling takes. The same override also checks for, and defers to, a subclass’s own__reduce__before building that hard-coded tuple – without that check aDeferredsubclass overriding__reduce__had it silently ignored at protocol 0 and 1 while protocol 2 and above (viaobject.__reduce_ex__unchanged) kept honouring it (#2433, #2435).
pcapkit.interface¶
Added¶
pcapkit.interface.registry, making public the registry-entry helperssame_entry,restore_shippedandrestore_shipped_dumper(#1519, #1568).
Fixed¶
the messages the interface functions and the helpers behind them raise and warn read in the house
<prefix>: <lowercase description>: ... {value!r}form.reassembleandtracelowercase their unsupported-protocol error and quote the protocol;follow_tcp_streamwarnsusing default engine instead, the wordingExtractoralready used, and quotes an engine that has no reassembly adapter;TraceFlowBase’s output-format warning is lowercased; andExtractor.make_namequotes the format it does not know, where an empty one ended the message at the colon with nothing after it (#2303, #2374).
pcapkit.protocols¶
Added¶
ESP parsing and construction [RFC 4303], with optional payload decryption and ICV verification through
cryptography(pip install pypcapkit[crypto]) (#378). Keys reach the dissector through a new caller-state channel,pcapkit.corekit.context, surfaced as thecontext=keyword onextract()andExtractor, carrying anesp.protocol.SecurityAssociation. Nothing here raises: with no association the SPI and sequence number are still reported and the ciphertext is left opaque withstatus=NO_SA, and a failed decryption or ICV check is recorded on the parsed result the same way. Extended Sequence Numbers, TFC padding and anti-replay are not implemented, and an unsupported cipher or MAC is refused with a clear error rather than half-processed.SCTP as a transport protocol [RFC 9260]: every chunk type, chunk parameter and error cause it defines, with the CRC32c both recorded and verifiable (it covers the SCTP packet alone, with no IP pseudo-header). Upper layers register on the DATA chunk’s Payload Protocol Identifier through
register_sctp, not on a port (#379).NGAP over SCTP (3GPP TS 38.413), decoding aligned PER through
pycrate(pip install pypcapkit[NGAP]) (#251, #417). Decoding is generic by ASN.1 shape rather than per-procedure, so every elementary procedure and protocol IE works and a new 3GPP release needs no code change. Registered as a default on PPID 60 and 66, but only 60 decodes: PPID 66 is an NGAP PDU inside a DTLS record, and there is no DTLS dissector.pycratewas at first left out of theallextra – it is LGPL-2.1+ where this package is BSD-3-Clause, and lands some 238 MB to obtain one module – until #910 counted it amongall’s core addons, soallinstalls it (theallentry under Project infrastructure).the Mobility Header registry, completed (#383, #437): the RFC 5568 fast-handover messages and options, then every registered message data type, every registered option (with nested sub-option registries for the flow identification, access network identifier, quality-of-service and LMA-controlled MAG parameter families) and every CGA extension. The CGA Parameters option, the only option that can carry a CGA extension on the wire, was the last to come off the generic handler, which is what let the CGA extensions round-trip end to end.
dispatch entries for dissectors that existed but were reachable from no registry (#436): FTP-DATA on TCP 20, HTTP/1 on TCP 8080, HTTP on UDP 8080,
L2TPv2on UDP 1701 and OSPF atTransType89.VLANbecame an abstract base withC_Tag(802.1Q) andS_Tag(802.1ad) as concrete subclasses, so a Q-in-Q frame no longer collapses into one opaqueRaw;L2TPlikewise became a base, withL2TPv2carrying the RFC 2661 implementation.Protocol/ProtocolBasegain acode=class keyword for the next-layer dispatch registries (Link.__proto__,Internet.__proto__,TCP.__proto__,UDP.__proto__,SCTP.__proto__,Frame.__proto__,PCAPNG.__proto__), the opt-in treatment #514 gaveEngine,Reassembly,TraceFlowandDumperabove, extended to the one family that needed a registration key invented. Omittingcodeleaves a subclass unregistered, as before; the built-in tables are still filled by literal assignment in each layer module, so nothing the library ships moves.codeaccepts a bare enum member, whose type infers the destination (EtherTypemeansLink,TransTypemeansInternet,PayloadProtocolIdentifiermeansSCTP, andLinkTypemeans bothFrameandPCAPNG, mirroringregister_linktype), or a{destination: key}mapping, required for a rawintsuch as a port number, which cannot say which transport it belongs to. Either form may appear in an iterable, so one declaration can register a class into several registries (e.g. aL2TPsubclass reachable by IP protocol number and by a UDP port), and the mapping form is accepted even where inference would work. Inference refuses rather than guesses: an enum member whose type names no known destination raisesRegistryError, and an unrecognised class keyword raisesUnsupportedCall, matching the other registrar families. Backed bypcapkit.foundation.registry.protocols.register_protocol_code, also callable directly for a class that declined at definition time. It was written up as the mechanism #548 needs; #548 turned out to need a missing class instead (115 is RFC 3931 L2TPv3 over IP, not RFC 2661; see the corresponding Fixed entry below), so its worked example namesL2TPv3(#570).tests/protocols/test_dispatch_reachability_unit.py, the coverage #548 asked for: everyProtocolBasedescendant whose__index__returns an enum member must be reachable under that code in the registry its enum type designates, read from the same__proto_dest__declaration backingcode=so the two cannot drift. It walks the classes rather than the registry entries, so it catches a class nothing registered, the shape in whichOSPFonce shipped (#548).tests/protocols/transport/test_tcp_mptcp_join_flag_ordering_unit.py, covering all three MP_JOIN layouts through the public constructor, the construct-pack-parse cycle for each, the stale-flags case that rules out a zero-valued default, the statement order, and controls for the parse path and the flag-independent options. The old cases bypassed the public path, so full coverage coexisted with a broken one. The staletcp-mptcp/MP_JOINentry is deleted fromEXPECTED_FAILURES(#587).tests/protocols/test_option_generator_tcp_base_unit.py, asserting that everyTCP_BASEkey is a parameterTCP.make()declares (derived frominspect.signature) and that the built segment carries the stated header in both data model and packed octets (#602).ILNP nonce sizing coverage in
tests/protocols/internet/test_ipv6_extension_unit.py, a test per protocol, asserting the declared length, the exact packed octets and the construct-pack-parse cycle over a spread of nonces, most with a bit length that is not a multiple of eight, where floor division and the ceiling disagree (#601).SOLUTIONparameter width coverage intests/protocols/internet/test_hip_unit.py: a spread of widths, each asserting the declared length, the reader’s acceptance and the construct-pack-parse cycle, plus a 57-bit case pinning the 20-octet length RFC 5201 Section 5.2.5 requires of HIPv1. Some widths are deliberately not multiples of eight, where the defectiveceil(bits / 4)differs from the correct width (#608).the BSD loopback layer for
LINKTYPE_NULLandLINKTYPE_LOOP, and dissection ofLINKTYPE_RAWby the version nibble (#1594).one home for each RFC- or format-fixed constant spelled out in more than one module – the IPv4, IPv6, AH and TCP header lengths and offsets,
IP_MAX_LENGTH, and the PCAP and PCAP-NG framing constants – in the module that owns the protocol (#1739, #1752).
Changed¶
a breaking change to
OSPF,RARPandDRARP: they move frompcapkit.protocols.linktopcapkit.protocols.application, with no deprecated re-export at the old path (#719). Layer is decided by designed function, with the IETF as the single source of truth – RFC 1812 Section 7 places routing protocols in the application layer and RFC 1122 Section 1.1.3 lists RARP there – and the rule is recorded indocs/source/contributing/conventions/protocol-layer-placement.rst.Import paths, before to after:
pcapkit.protocols.link.ospftopcapkit.protocols.application.ospf.protocolpcapkit.protocols.data.link.ospftopcapkit.protocols.application.ospf.datapcapkit.protocols.schema.link.ospftopcapkit.protocols.application.ospf.schemapcapkit.protocols.link.rarptopcapkit.protocols.application.rarp.protocol
from pcapkit import OSPFandfrom pcapkit.protocols import OSPF(likewiseRARPandDRARP) are unaffected;from pcapkit.protocols.link import OSPFand the deep paths above break. RARP has nodataorschemamodule of its own, as it reuses ARP’s.layerchanges from'Link'to'Application'for OSPF, RARP and DRARP.OSPFnow takesApplicationas its only base, andRARPbecomesclass RARP(Application, ARP)– layer base first, becauseARP’s chain reachesLink, which owns__layer__.ARPandInARPstay'Link'. Dispatch keys are unchanged: OSPF is still reached fromInternet.__proto__atTransType.OSPFIGPand RARP fromLink.__proto__by EtherType. LeavingLinkrepointsOSPF.__proto__atProtocolBase.__proto__, so OSPF no longer sees Link’s EtherType entries – which nothing used, since it is reached by protocol number, and a-1lookup falls back toRawin either registry without inserting.registerand_read_protosstill resolve, onProtocolBaserather thanLink.RARPkeeps Link’s throughARP.Layer-limited extraction:
layer='link'no longer sets the termination flag on OSPF andlayer='application'now does – the same inversion applies to RARP – but the extracted protochain is unchanged at everylayer=value, including'internet'(IPv4:OSPFIGPbefore and after, as IPv4 ends the chain itself).renames with no compatibility alias left behind: PCAP-NG
Optionsubclasses spell the namespace class keywordns=instead ofnamespace=(#439).tests/protocols/transport/test_tcp_udp_unit.pynow reaches the MP_JOIN dispatchers throughTCP()itself, instead of assigning a Pythonsetto_flagson a bareTCP.__new__(TCP). Asetanswers the same membership tests, so every branch ran while the attribute had neither the type production assigns nor the ordering that governs when it exists – how #587 stayed invisible. Reverting #587’s hoist, or restoring thecast('Enum_Flags', 0)seed, now fails the file. Test-only (#603).building a protocol through its constructor with a keyword that names nothing now raises
UnsupportedCallinstead of discarding it. This is a behaviour change to a public API: everymakein the tree ends its signature with**kwargsand reads nothing out of it, so a misspelled keyword was accepted, dropped, and the field it named kept its default – wrong octets, nothing said, as in #602, #541 and #556. The check sits inProtocolBase.__init__()rather thanmake, because__post_init__passes one**kwargsto the construction and to the parse of what it has just constructed, so a keyword declared only byreadlegitimately travels throughmake(HIP.read()declaresextension). The accepted set is the union of every keyword-taking parameter ofmake,read,pack,unpack,__post_init__and__init__across the MRO. Parsing is deliberately untouched: a protocol cannot know which keywords its parent passed on. A new__keywords__opts a class out: a set, for a keyword read out of**kwargsby name (ESP.read()withpacket); andNone, for a dispatcher whose real signature belongs to a class chosen at call time (onlyHTTP.make). The message names the near neighbour (seqreports did you mean ‘seq_no’?).from_datawarnsUnknownFieldWarningrather than raising for a key its own_make_datareturned, which the caller cannot fix; a keyword passed tofrom_dataitself raises, as for a direct construction (#1147). The leftover copies of #602’s misspelled TCP base, inexamples/generators/dispatch.pyandtests/protocols/transport/, are fixed. Some_make_datakeys thatmakedoes not declare are reported, not fixed:Frame._make_data()returnsts_srcwheremakedeclarests_sec,L2TPv2._make_data()returnspriowhere it declarespriority, andHeader._make_data()returns amagic_numberthatHeader.make()does not take. A directSomeProtocol.make(...)call is not checked and still discards silently (#617).a breaking change to
Application: it no longer forbids payload dispatch outright. It encodes “no further protocol layer above this one”, which undissected trailer bytes are not, so_decode_next_layerand_import_next_layeraccept the-1sentinel – the rest of the packet, resolving toRaw, or toNoPayloadwhen nothing remains – and__post_init__keeps a payload thatreadset instead of overwriting it withNoPayload. A real protocol number (anyprotoother than-1) is still refused withUnsupportedCall. A subclass that calls either method with-1changes from raising to succeeding; no subclass in the package does (#719).the
ProtocolandProtocolMetadocstrings.Protocolclaimed to be the base of every protocol family, though nothing in the package subclasses it, andProtocolMetaclaimed to generate attributes its empty body does not.Protocolis now documented as the base for third-party protocols, andProtocolBaseas the one the families derive from (#1101).a breaking change to
OSPF_CrytographicAuthentication: the misspelling is corrected toOSPF_CryptographicAuthentication, andCrytographicAuthenticationinpcapkit.protocols.application.ospf.schemaandpcapkit.protocols.application.ospf.datatoCryptographicAuthentication, with no alias left for the old spelling. Both OSPF names also move to the application group ofschema.__all__anddata.__all__, after #719’s move (#1090).a breaking change to a declared length that runs past the data: it is refused with
ProtocolErrorrather than accepted and zero-filled on rebuild, so a malformed option looks different ininfo(#1325, #1398).dissection is bounded: a frame at 64 layers (#1610, #1620) and an IPv6 extension-header chain at 32 headers (#1604, #1608). Both limits are
PCAPKIT_FRAME_LAYER_LIMITandPCAPKIT_EXTENSION_HEADER_LIMITinpcapkit.utilities.config(#1743). Rebuilding a frame cut at the chain bound warns once (#1619, #1630).a breaking change to IPv6 extension headers: an unrecognised or unparsable one is parsed generically, per RFC 6564, rather than collapsing the packet to
Raw(#891, #904).IPv6_Extdoes it, and is also the shared base of every extension header (#917, #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 leftschemadisagreeing withinfo:UDP(srcport=1234).pack()resetschema.srcportto 0 whileinfo.srcportkept 1234. Once an instance has been parsed or constructed,pack()builds on a shallow copy, soschema,infoand whatmake()sets, such as TCP’sconnection, stay as they were. TheFrame,PCAPNGandHTTPv2overrides do the same, and re-packing a PCAP-NG block that carries an option allowed only once no longer raises. Code that relied onpack()to re-syncschemacallsmake()or constructs a new instance (#1769, #1791).a breaking change to the HOPOPT, IPv6-Opts and Mobility Header padding: two octets are one
PadNwith no data (01 00) rather than twoPad1options, as RFC 8200 Section 4.2 and RFC 6275 Section 6.2.2 ask. Only a freshly built header with exactly two octets of padding changes; a parsed one that carried twoPad1options still rebuilds byte for byte (#1793, #1812).a breaking change to
NTPTimestamp: it andPMIPv6TimestampareInfoclasses inpcapkit.protocols.internet.mh.datarather than tuples, andNTPTimestampkeeps no alias at its 1.4.x home inpcapkit.protocols.internet.mh.protocol. As Mappings they no longer index, unpack, compare or hash as a tuple does;.secondsand.fractionstill work, and the dumpers write both as keys (#1886, #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 andlen()carry the dunder keys a newInfo.__included__names, andInfo.__excluded__only keeps a key out ofrepr,strand the dumps. A hand-written dict needs those keys,__short_read__among them, to rebuild, and a customInfolists its own dunder keys in__included__(#2003, #2014).a breaking change to
PCAPNG.context: the block’s sectionContext, which it returned from 1.0.0 through 1.4.x, isPCAPNG.section_context, with no alias, andcontextis the caller-suppliedContextRegistryevery protocol inherits fromProtocolBase, on a Section Header Block too.PCAPNG.section_contextraisesUnsupportedCallwherecontextdid: on a block that holds no section, such as a Section Header Block thePCAPNGengine has not assigned the section it opens. Code readingblock.contextfor the section readsblock.section_context(#2172, #2205).a breaking change to
FTP_DATA: it derives fromApplicationas well asRaw, so its__layer__names the application layer where it wasNone– alayer='application'extraction stops at it, andextract(layer=FTP_DATA)resolves to that layer rather than to'none'. Raw still does the parsing and the rebuild, and__index__is the application layer’s, raisingIntErrorwhere it raisedUnsupportedCall. The class moves topcapkit.protocols.application.ftp_data.protocolwith nothing left behind on the old path inpcapkit.protocols.application.ftp.protocol, where it shipped from 1.0.0 through 1.4.1 (#2285, #2357).a breaking change to HIP’s
REG_FAILEDparameter: its first octet is the Failure Type, and it carries no lifetime (RFC 8003 Section 4.5), sopcapkit.protocols.internet.hip.data.RegFailedParameterreplaceslifetimewithfailure_type, aRegistrationFailure, with no alias, and itsreg_typeholdsRegistrationvalues rather than failure types.HIP._make_param_reg_failed()takesfailure_typein place oflifetime, andreg_failedtakes registration types. The octets are unchanged (#2309).a breaking change to the HTTP/2
RST_STREAMandGOAWAYbuilders: a frame constructed withouterrorcarriesNO_ERROR(0x0), the code RFC 9113 Section 7 gives a condition that is not the result of an error, rather thanHTTP_1_1_REQUIRED(0xd), which asks the peer to use HTTP/1.1 instead. Amakethat omitserroremits different octets; one that passes it is unchanged (#2311).
Fixed¶
next-layer, option, chunk, block and parameter dispatch all read
defaultdictregistries, so a lookup miss inserted the key into class-level state shared by every later instance, after which a legitimateregister_*call warned that the code was already registered. Every read now goes through a lookup that does not grow the table, andIPv4.__option__andHIP.__parameter__became inspectable class attributes rather than names assembled at call time (#426, #428, #429, #434). What breaks: a tuple-registered handler pair written to the documentedOptionParser/OptionConstructorsignature now works where it could never be called, and a pair written with an explicit leadingself– the only shape that used to work – now does not.the identical defect one layer up, in the schema layer’s
EnumSchema.registry:Option.registry[code]for an unregisteredcodeinserted the default schema under that code, so a single lookup made an unassigned TCP option number such as156read back as registered for the rest of the process.EnumSchema.__enum__is now built (or, when a subclass seeds it manually, asPCAPNG.Option’s namespaced mapping andTCP.MPTCP’s plain one do) as a retention-safe mapping that still returns the registered default on a miss but stops recording it;.registryreturns the same object as before, so nothing holding a reference is affected (#555).on Python 3.10 and older, no
Schemasubclass got its own_abc_impl: all fell through tocollections.abc.Mapping’s, so a singleisinstanceorissubclassanswer poisoned every later question about that class for the rest of the process. A terminating PCAP-NGEndRecordtestedTrueas anIPv4Record(#439).construction, broken in several places at once: the generated typed
__init__was never installed, so__post_init__did not run and a schema built from a subset of its fields could not be packed –UDP(srcport=53, dstport=5353)now packs (#430); IPv6 and Mobility Header option padding was wrong, leaving construction wholly broken (#398);HTTP.makecalled the versionedmakeunbound, so every real call raisedTypeError(#452, #462); andIPv4._make_data()returned the fragment offset in octets where the wire wants 8-octet units, and readdata.optionson a packet that has none (#494, #499).a truncated or under-declared area no longer parses “successfully”, and no longer wedges the process. The option and list loops could spin forever with no exception on a truncated area, reachable from untrusted input through HOPOPT, IPv6-Opts, MH, HIP and SCTP; each iteration must now advance the stream by at least one octet, and the error names the option, the offset and the octets remaining (#431, #432). Separately, wire-derived lengths in
ipv6_opts, CALIPSO, MPL, REG_INFO and HIP list callbacks underflowed below zero, whichListField’s ownwhile length > 0turned into a silent empty list; they are floored and raise instead (#449, #456, #460, #463).field widths and units, each measured against the specification: HIP’s
TRANSPORT_FORMAT_LIST,NAT_TRAVERSAL_MODEandESP_TRANSFORMlist entries are two octets, not one [RFC 7401, RFC 5770, RFC 7402] (#463, #472); the MN-ID option sizes from its subtype, not fromidentifier’s Python type (#448, #464, #467); IPv6-Route’sHdr Ext Lenis computed in 8-octet units on both sides [RFC 8200] (#487, #489); and the Fast Binding Update and Acknowledgment Lifetimes are plain seconds [RFC 5568], not RFC 6275’s four-second units (#502).the remaining call sites where a
_make_*method converts an address itself, ahead of the schema, so the samebool-as-intmistake reached them, unguarded by the schema-layer fix above:ARP._make_proto_resolve,IPv6_Route._make_data_type_rpl()(which also derived its RPL compression lengths from the laundered value, corrupting the address list), thedstparameter ofIPv6_Route.make(), and the latentOSPF._make_id_numbers. Each now goes throughparse_ip_address, the helper #539 and #552 route their own sites through, completing the programme #481 and #508 started. Pinningversion=6on theIPv6_Routesites is a further, smaller behaviour change: both converted a plain integer through the family-inferringipaddress.ip_address, soip=[258]packed a 4-octet IPv4 address (0.0.1.2) inside an IPv6-only header; it now packs the 16-octet::102(#540).PCAP and PCAP-NG output and parsing:
bytes(frame)returned the next frame’s octets,files=Truewrote names likeFrame 1..json, and a PCAP-NGtimestamp_epochwas shifted by the reading host’s timezone (#403); further parser defects (#341–#347, #371) and, on the write path, more block-parsing ones (#388);BitFieldpacked every named bit as set (#359, #374); the extension-header walk failed to advance past the last IPv6 extension header (#348, #373); and IPv6 fragment offsets went unscaled, with reassembly keyed on the flow label – optional, and routinely zero, so distinct datagrams collapsed together – rather than on the fragment identification (#389).TCP._make_mptcp_addaddr()could not build anADD_ADDRoption end to end: itskind=/length=arguments were rejected withUnknownFieldWarningand dropped, and.pack()then raisedKeyError: 'length'fromport’s own condition,pkt['length'] in (10, 22). The cause was one layer up:MPTCP, the base of every Multipath TCP subtype schema, declaredkindandlengthonly undertyping.TYPE_CHECKINGrather than as real fields, unlikeOption. That droppedkind=/length=for every_make_mptcp_*constructor, soMPTCPnow declares both for real (#541). The missing fields broke parsing too: a subtype schema’s leading field read thekindoctet itself, an off-by-two in field alignment rather than a wire-format change (a correct sender’s octets were always right). Spec-correctADD_ADDRandMP_PRIOoptions failed withFieldError: TCP: [OptNo 30] 3 invalid IP versionandKeyError: 'length'; both parse now.dropped-keyword/wrong-cast defects flagged in review during this release and filed only as #556: HIP’s
_make_param_encryptedpassedcipher=to a schema with no such field, so an AES-cipherENCRYPTEDparameter built throughmakepacked without its IV; and IPv6-Route’sRPL.post_process(), which runs on everySchema.pack(), assumedself.addresseswas still the concatenatedbytesa parse leaves, and raised slicing thelist[bytes]amake-built multi-address header holds (#556).further defects #541 exposed by letting construction reach code that had never run.
MPTCP.subtypewas stilltyping.TYPE_CHECKING-only, soTCP(options=[(Enum_Option.Multipath_TCP, ...)])raisedAttributeError: ... has no attribute 'subtype'for every subtype butMP_JOIN: the convenience constructor reads the schema straight back through_read_mptcp_*with no byte round trip, so_MPTCP.post_process(), the only code that setsubtype, never ran. Fixed on the construction path (TCP._make_mode_mp()) rather than by makingsubtypea real field too, the waykind/lengthbecame in #541:subtypeis already packed as 4 bits of each subtype’s owntestbitfield, and a second field for the same bits would double-encode them or need a “derive, don’t pack” field kind this library lacks (#566). Separately,_make_mptcp_capablewrotelength=20 if rkey is None else 32where RFC 8684 section 3.1 gives 12 and 20, andMPTCPCapable.rkey’s condition (pkt['length'] != 32) dropped the receiver’s key for exactly the length the maker used for “key present”: a key-absent MP_CAPABLE could not be built, and a key-present one lost its key on the wire. Both, and the guard in_read_mptcp_capable, now agree on 12/20. This changes MP_CAPABLE’s packed output: a 20-octet key-present option built or parsed under the old code becomes 12 octets with no key, or 20 octets with the key present, depending on which the caller meant (#567).TCP’s MP_CAPABLE now parses and builds all five lengths of RFC 8684 section 3.1 – 4 (SYN, no key), 12, 20, 22 (adds the Data-Level Length) and 24 (adds the Checksum) – where
_read_mptcp_capableraisedProtocolError: TCP: [OptNo 30] invalid formatfor anything but 12 and 20. In the schemaskeyis now present only forlength > 4andrkeyforlength > 12, and the fieldsdl_len(16 bits,length > 20) andchecksum(2 octets,length > 22) are new._make_mptcp_capabletakesdl_lenandchecksumtoo, where it used to drop them silently through**kwargs, and picks the length from how many ofskey,rkey,dl_lenandchecksumare notNone. A field given without the one before it raisesProtocolError. The data modelMPTCPCapablegainsdl_lenandchecksum, andskeybecomesOptional[int]: code that constructs it directly must now pass both new fields, andto_dict()carries the new keys (#1042).IPv6-Route’s RPL routing data was five octets wide at the front where RFC 6554 section 3 gives four, with further defects behind it.
CmprI,CmprEandPadare each 4 bits sharing a 32-bit word with a 20-bitReserved, but the schema declaredcmpr_iandcmpr_eas whole octets, so a constructed two-address header packed to 41 octets while itsHdr Ext Lenof 5 declared 48. Correcting the width is a wire-format change and reshapes the schema:RPL(cmpr_i=..., cmpr_e=...)is nowRPL(cmpr={'cmpr_i': ..., 'cmpr_e': ...}), beside the existingpad={'pad_len': ...}. Behind it, the reader’sheader.length % 16guard readHdr Ext Lenas an octet count and assumed 16-octet addresses, which an SRH carries only whenCmprIandCmprEare both 0 – the unit confusion #487 fixed for Source Route and Type 2, left by #489 for want of a working RPL round trip to validate against. It is replaced by section 4.2’s address-count arithmetic,n = (((Hdr Ext Len * 8) - Pad - (16 - CmprE)) / (16 - CmprI)) + 1, which the reader now requires to be non-negative and whole.RPL.post_process()subtractedpad_lena second time from a buffer whose length callback had already taken it off, losing one address per16 - CmprIoctets of padding, and setiponly when it had parsed octets, soIPv6_Route(type=..., data={'ip': [...]})raised a bareAttributeError: 'RPL' object has no attribute 'ip'. And_make_data_type_rplcomputedPadas8 - length % 8without the outer% 8, so an aligned address vector got a full 8 octets of padding where section 3 requiresPadto be 0 whenCmprIandCmprEare both 0. The narrowercmpr_ialso removes a latent divide-by-zero (a whole octet could hold 16, making16 - CmprIzero).ipv6-route-type/RPL_Source_Route_Headerround-trips and itsEXPECTED_FAILURESentry is deleted; none of this has been checked against a real RPL capture (#564).L2TPv2parsed any version nibble, so an L2TPv3 datagram was reported as v2 with a tunnel and session ID read out of v3’s Control Connection ID. RFC 2661 §3.1 fixesVerat 2 and reserves 1 for L2F, andL2TPv2.versiondocumented that any other value “is a different protocol reached through a different class”, but nothing enforced it, so the hard-codedLiteral[2]property andinfo.versionanswered 2 and 3 on the same octets.readnow raisesProtocolError, degrading the payload toRawthrough the existingbeholderpath with the reason recorded. This affects real captures: RFC 3931 §4.1.2 puts L2TPv3 on port 1701 too, the portUDP.__proto__binds, soEthernet:IPv4:UDP:L2TPv2:Rawwith invented field values becomesEthernet:IPv4:UDP:Rawwith the octets preserved.This resolves #548, which reported
TransType.L2TP(115) as registered nowhere and proposed bindingL2TPv2there. That binding is wrong rather than awkward: RFC 3931 §4.1.1 gives 115 to L2TPv3 over IP, whose session header carries no version nibble at all, so a v2 parser cannot detect that the datagram is not its own. 115 is a missing class, not a missing registration, and stays unbound until anL2TPv3class exists; no dissector was invented to fill it. The reasoning is recorded inpcapkit.protocols.link.l2tp.protocol, andregister_protocol_code’s worked example, which namedL2TPv2at 115, namesL2TPv3instead (#548).building any
MP_JOINoption raisedAttributeError: 'TCP' object has no attribute '_flags'.TCP.make()constructed the options before assigning theself._flagsthat the option makers read, and_make_mptcp_joinbranches on it to choose among the three layouts of RFC 8684 section 3.2 (figure 5, SYN, 12 octets; figure 6, SYN/ACK, 16; figure 7, ACK, 24). The parse path was never affected, sincereadassigns the flags first. Fixed by hoisting the flag resolution above the_make_tcp_optionscall, leaving only the data-offset computation, which needs the options’ total length, after it. Not fixed by a zero default for_flags, which would have been worse:Protocol.pack()is public and callsmake, so an instance that had parsed a segment had the attribute set and silently built the option for the segment it had read. A parsed MP_JOIN-SYN instance asked to pack an MP_JOIN-ACK segment emitted an ACK header carrying figure 5’s 12-octet SYN option, with the caller’s 20-octet HMAC replaced by an all-zero token and nonce, and nothing raised. Hoisting also made a branch reachable for the first time, an MP_JOIN on a segment with neither SYN nor ACK: the accumulator was seeded withcast('Enum_Flags', 0), andtyping.castbeing a runtime no-op,self._flagsstayed a plainintand the first membership test raisedTypeErrorrather than the documentedProtocolError. It is now seeded withEnum_Flags(0), a real flagless member that equals0and ORs identically. The read path’s seed is unchanged: a flagless MP_JOIN is rejected bymptcp_data_selectorbefore_read_mptcp_joinruns (#587).the ILNP Nonce option builders in
HOPOPTandIPv6_Optssized the option withmath.ceil(nonce.bit_length() // 8), floor division dressed as a ceiling (//floors andmath.ceilof anintis a no-op). The nonce is packed by aNumberFieldwhose width is the declaredlen, so an under-declared length silently truncated the nonce on the wire. Every nonce whose bit length was not a multiple of eight was affected, and small values were the worst case rather than boundary values: any nonce below 256 was declared as zero octets and dropped (nonce=9packed tob'\x8b\x00'and parsed back as0). Fixed tomax(1, math.ceil(nonce.bit_length() / 8)), the form used at the other sizing sites inhip.pyandmh.py. The one-octet floor is themh.pyconvention and is load-bearing becausenoncedefaults to0, whose bit length is0: without it the default builds an option with no Nonce Value field at all, collapsing “the nonce is 0” into “there is no nonce” when RFC 6744 gives the option that field. The read path takes its width from thelenoctet on the wire and was never affected (#601).httpv1’s_RE_METHODwas unanchored andre.matchanchors only at the start, so it prefix-matched, and the request-line reader passed the wholepara1toMethod.get()rather than the capturedmethodgroup. Sob'Get'matched on the single characterG, satisfied the guard that decides a start-line is a request, and handed the mixed-case token to a lookup that raised on it. Fixing either half alone still gives a wrong answer (normalising the lookup would parseb'Get'asGEToff a one-character match; passing the group would parse it as a method namedG). The pattern is anchored at both ends and the captured group is looked up, so a non-method token is a malformed request line rather than a mis-parsed one. Method tokens are case-sensitive per RFC 9110 Section 9.1, so nore.Iwas added:GETparses,Getandgetare rejected (#583)._RE_STATUSin the same reader had the same unanchored prefix defect, found by auditing_RE_METHOD’s siblings, and escaped as the wrong exception type. The pattern is only a guard (the value comes fromint(para2)on the raw token), so a prefix match let a malformed status past the guard and out ofint()uncaught, where_read_http_headerdocumentsProtocolError:200xraisedValueError: invalid literal for int()and2000raisedValueError: 2000 is not a valid StatusCode; both are nowProtocolError. RFC 9112 Section 4 givesstatus-code = 3DIGIT, exactly three, so the anchor is what the grammar said (the production lives in HTTP/1.1 becausestatus-codeis part of itsstatus-line; RFC 9110 Section 15 covers code semantics and the registry)._RE_VERSIONwas audited and is safe, since every call site reads the captured group (#583).as a consequence of the above, the unhandled
MemoryErrorinpcapkit/protocols/protocol.py’sself._file.read()call on a truncated PCAP-NG capture.dhcp_little_endian.pcapngcut to 161, 641 or 1389 octets left a one-octet read of a little-endian 32-bit block length, whichrjust()turned into0x78000000or0x84000000(1.88 to 2.06 GiB) and passed straight toself._file.read()as an allocation size, from a 1772-octet file. Under a 1 GiB address-space cap all three raiseMemoryError; with more space the parse fails anyway. Withljust()the same reads report 120 and 132, no large allocation is attempted, and all three end in the ordinary handled parse failure (#604).reading a big-endian classic PCAP byte-swapped every record header field, and then crashed.
Frame.unpack()seeded the file’s byte order under the keybytesorderwhere the schema’sbyteorder_callbackreadsbyteorder, so the lookup never found it and fell back tosys.byteorder, the host’s order rather than the file’s. On a little-endian host reading a little-endian capture that is right by coincidence, and every capture in this repository was little-endian. Frame 1 ofbig_endian.pcapreadincl_len=1241513984for a record whose real value is74.incl_lenis the payload length, so that record consumed the whole file and the second was read with a negative length, raisingValueError: read length must be non-negative or -1– the reported crash, and the second symptom. The siblingFrame.pack()spelled the key correctly, marking this a slip rather than a second key, and the fallback made a misspelled key indistinguishable from an absent one;byteorder_callbacknow records that it defines the key and why the fallback hides a typo (#605).documentation.
mptcp_dss_ack_selector’s note said a corrected field-width lambda “would not have worked” and that fixing it belonged topcapkit.corekit.fields.numbers, which is where #598 then fixed it; the same paragraph sat intest_tcp_mptcp_length_arithmetic_unit.py’s docstring, whose other stale claim was that MP_JOIN “cannot be built through the publicTCP()constructor at all”, true only until #587. A callable-lengthNumberFieldpacks and unpacks both DSS widths now, and wire absence was never the obstacle:MPTCPDSS.ssn,dl_lenandchecksumhave always beenConditionalFieldon the siblingMflag. TheSwitchFieldform is kept for the narrower reason the note now gives:ConditionalField’slengthforwards to the wrapped field without consulting the condition, whereas aSwitchFieldalways resolves to a concrete field (#603).the assertion #604 left pinning the old padding side, which fails once #621 pads with
ljust().TCPUDPUnitTests.test_a_truncated_option_still_parses_its_declared_lengthexpected a truncated TCP option’sdataas the synthesised zero octets followed by the real ones, asrjust()produced:Reserved_79declaringlength=12over 6 real octets reportsaabbccddeeff00000000where the test expected00000000aabbccddeeff. The real octets now come first, and the docstring is corrected. Test-only; the sibling case intests/protocols/internet/test_ipv4_unit.pywas retargeted in #621 (#604).the HIP
SOLUTIONbuilder sized the parameter with4 + math.ceil(max(random.bit_length(), solution.bit_length()) / 4), an invalid shorthand for two fields. RFC 7401 Section 5.2.5 givesLengthas4 + RHASH_len / 4over aRandom #Iand aPuzzle solution #JofRHASH_len / 8octets each, and/ 4equals twice/ 8only becauseRHASH_lenis a whole number of octets. For an arbitrarybit_length()the builder emitted an odd contents width, which its own reader refused:_read_param_solution’s(schema.len - 4) % 2guard exists becauseSolutionParametersplits that width into two equal halves.random=0x1withsolution=0xfffdeclaredlen=7and raisedProtocolError: HIPv2: [ParamNo 321] invalid formaton input the library had produced. The undersized length was worse: both fields take their width from thatlen, sosolution=0xfffwas packed into one octet and came back as0xff, silently. Fixed to4 + 2 * math.ceil(max(...) / 8), the form the siblingPUZZLEbuilder uses, which is even by construction. Loosening the reader was rejected: an odd contents width has no meaning in the wire format (the RFC makes the fields equal-width and does not say which takes an extra octet), and a laxer guard would still have truncated the value (#608).TCP.read()seeded its connection-flag accumulator withcast('Enum_Flags', 0).typing.castis a runtime no-op, so the accumulator began as the plainint0, and only the|=promoting it to apcapkit.const.tcp.flags.Flagsmember ever changed that. A segment with an all-zero flags octet (an nmap NULL scan) leftself._flagsanint, so a membership test raisedTypeError: argument of type 'int' is not a container or iterable, andTCP.connectionreturned anintwhere it annotatesFlags. Any flag masked it, which is how it survived 100% statement and branch coverage. The seed isFlags(0), as #597 already did for the sibling accumulator inmake;Flagsdeclares no_missing_, soFlags(0)is an ordinaryaenum.IntFlagpseudo-member and does not meet theRecursionErrorthe same construction hits on thepcapkit.const.mhflag enumerations (#623). The MP_JOIN path was latent:mptcp_data_selectorrejects a flagless MP_JOIN before RFC 8684 section 3.2’s three layouts are chosen. A dump-format change is observable: a flagless segment’sconnectionrenders as a string where it rendered as the number0, so it no longer switches JSON type with the flags. That string readFlags::None [0]until thepcapkit.dumpkitfix above (#616).a breaking change to a public attribute.
Frame.lenandFrame.cap_lenwere filled from opposite wire fields depending on the container format, soframe.lenmeant the captured length out of a.pcapand the on-wire length out of a.pcapng. The PCAP reader moved to match PCAP-NG:lenis the on-wire length (the record header’sorig_len) andcap_lenthe octets stored (incl_len). Code readingframe.lenorframe.cap_lenfrom a.pcapgets 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 in25f216f4(2023-04-27), both internally consistent. The names settle it, being Wireshark’s:frame.lenis “Frame length on the wire” andframe.cap_len“Frame length stored into the capture file”, andframe.len_lt_caplenexpert info flagsframe_len < cap_lenas malformed, which it could not be iflenwere the smaller, captured one.Frame.read()hands_decode_next_layerthe octets present, nowframe.cap_len; handing over the on-wire length would be the declared-length-exceeds-available-octets fault of #554, #573 and #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 (#618).the HIP
PUZZLEandSOLUTIONbuilders derived wire-format values from the payload value instead of the data model, and each was wrong. This changes thePuzzleParameterandSolutionParameterdata models and the octets both parameters emit. The width ofRandom #IandPuzzle solution #Jcame fromint.bit_length()alone, so every leading zero octet was dropped on re-serialisation: aSOLUTIONread withLength = 20rebuilt asLength = 6, aPUZZLEwithLength = 12asLength = 5, silently. That width isRHASH_len / 8octets [RFC 7401 Section 5.2.4, RFC 7401 Section 5.2.5], a property of the Responder’s HIT Suite, so both data models now carry it asrhash_lenand both builders prefer it; a newrhash_len=keyword states it for a build from scratch, which under HIPv2 is the only place it can come from, sinceRSA,DSA/SHA-256is the REQUIRED HIT Suite [RFC 7401 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).SOLUTION’s second contents octet isReserved, “zero when sent, ignored when received” [RFC 7401 Section 5.2.5, and RFC 5201 Section 5.2.5 identically], not theLifetimethat only RFC 7401 Section 5.2.4 defines, which pcapkit encoded there as2^(value - 32)seconds. It wrote0x20,0x21,0x25or0x2binto a field the RFC requires to be zero, and could not write that zero: a conformantSOLUTIONwithReserved0x00parsed totimedelta(0)and could not be re-serialised, escaping a bareValueErrorfrommath.log2(0.0)that was not aBaseError. The field is renamedreserved, defaults to zero, and is carried verbatim across a round trip (#654). And neither builder read its ownversionkeyword, soversion=1andversion=2computed identical lengths, where RFC 5201 Section 5.2.4 and RFC 5201 Section 5.2.5 state both fields as literally 8 bytes andLengthas literally 12 and 20. Under HIPv1 each builder accepted only abit_length()of 57..64 and built, for anything narrower, a parameter this library’s reader rejects – the shape of #608, in thePUZZLEbuilder that #629 never opened (#655). The remainingmath.log2sites, inPUZZLEwhere a lifetime is real, raiseProtocolError((BaseError, ValueError), so a caller written around the bare exception still catches it). Migration:SolutionParameter.lifetimeis nowreservedand anintrather than atimedelta;PuzzleParameterandSolutionParametergained a requiredrhash_len; and_make_param_solutionno longer takeslifetime=. Each was reachable end to end only once #608 was fixed in #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, #654, #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 butLOCATOR_SET, excluded below. This changes the octets those parameters write, and thelengththeir data models report. RFC 7401 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 givesTotal Length = 11 + Length - (Length + 3) % 8; every padding site computed(8 - (Length % 8)) % 8, which aligns the contents alone. For everyLengthfrom 0 to 63 the result was never a multiple of eight and never the RFC’s value, erring both ways: atLength = 4(a wholeSEQ, which RFC 7401 Section 5.3.5 puts on everyUPDATE) pcapkit appended four octets that must not be there, and atLength = 8it 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 butLOCATOR_SET’s (thePaddingFieldcallbacks inpcapkit/protocols/schema/internet/hip.pyand the record lengths inpcapkit/protocols/internet/hip.py) now goes through oneparameter_total_lenand oneparameter_padding_len(#651).LOCATOR_SETkept the old expression because defects there cancelled exactly: its padding callback never received the parameter’slen(the nestedLocatorschemas share a packet context whose ownlenshadowed it, so it saw 4 for an IPv6 locator), and the parameter’slenwas written in 4-octet units where the RFC’sLengthis a byte count; the result,4 + 24n + 4, equalled the RFC total24n + 8. #679 later fixed both sites.EncryptedParameter’sdatalength callback is fixed in the same change: it subtracted the sixteenivoctets but not the fourreservedones, which cancelled the padding’s four atLength % 8in{0, 5, 6, 7}, so correcting the padding alone would have madeENCRYPTEDfour octets too long at all eight residues.HIP.make()’slen = total_length // 8 + 4is not part of the defect: RFC 7401 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: aSEQparameter’slengthis now 8 where it was 12, and the other parameters move likewise, so code comparing stored output byte for byte or asserting onData_*Parameter.lengthsees the RFC’s values.LOCATOR_SETis unchanged in both respects until #679 (below).examples/generators/options.py’sHIP_COPIESstayed at two forR1_COUNTER’s four-octetcounterwhere RFC 7401 Section 5.2.3 requires eight, which this defect had masked, until #672 widenedcounterand #679 fixedLOCATOR_SET’sLengthunit, after which #689 droppedHIP_COPIESto one (#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_datawas the only_make_http_*method that never readframe.flags, so a DATA frame parsed withEND_STREAMset rebuilt with the bit clear (flags octet0x01to0x00): a well-formed frame saying the stream continues where the capture said it ended [RFC 9113 Section 6.1].PADDEDwas re-derived frompad_len, and the sibling builders already restored theirs, so this was a missing read-back (#652). Separately,FrameType.post_process()seeded its flag accumulator with a bare0, which|=promotes only as a side effect, so a frame with no bit set left__flags__a plainintwhere the schema and data model declare aFlags, andEND_STREAM in __value__raisedTypeError: argument of type 'int' is not a container or iterablerather than answeringFalse. That is the common case: a0x00flags octet is every openingSETTINGS, every non-finalDATAand every non-ACKPING. It now seedsself.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 thepcapkit.dumpkitfix above gives a nameless member) where it read0, the same change #616 made to TCP’sconnection. The seed is guarded becauseFrameType.Flagsdeclares no members and, from Python 3.11, a memberlessenum.Flagsubclass refusesFlags(0), so the frame schemas that inherit it (UnassignedFrame,PriorityFrame,RSTStreamFrame,GoawayFrame,WindowUpdateFrame) keep the plainint, which nothing observes since each passesflags=None. A DATA round trip is still lossy for an unrelated reason found while measuring: payload length callbacks are mis-parenthesised, so an unpaddedDATA,HEADERSorPUSH_PROMISEframe parses with its whole payload dropped (#668, below) (#652, #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_joinand_make_mptcp_joinfor the invalid flag combination, and both guards of_make_mptcp_dssfor 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 existingassertInon the body passes either side of the change (#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:
Anyinpcapkit/utilities/logging.py, and inpcapkit/protocols/schema/internet/ipv6_route.pybothProtocolin apayload:stub andOptionalinside atyping.cast. Each is now in its module’sTYPE_CHECKINGblock,ProtocolspelledProtocolBase as Protocolas the sibling schema modules already do. Thecastcase is the one worth naming:typing.castnever 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_CHECKINGisFalse); making these annotations resolve at runtime is a separate decision about the idiom. A newtests/project/test_annotation_names.pypins 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 treatingLiteralmembers andAnnotatedmetadata as values; without the fix, it reports exactly those findings. The same change corrects theMANIFEST.incomment asserting thatinclude README.mdwas 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 entry above carried the same claim and is corrected with it (#642).every PCAP-NG packet block now carries the captured octets it declares. Every Enhanced Packet Block of the committed
examples/captures/dhcp.pcapngreportedlen(packet) == 0against acaptured_lenof 314, 342, 314 and 342, as did the Simple Packet Block and obsolete Packet Block (synthesised; no committed fixture has either). Those block types, exactlyPCAPNG.PACKET_TYPES, are the whole blast radius. The octets were read correctly and thrown away:PCAPNG.unpack()extracted them from the block schema, andProtocolBase.__init__()overwrote the result withself.packet.payload. The inheritedpacketsplits a protocol atself.lengthoctets of header and takes the rest as payload, which holds only for a header followed by its payload;PCAPNG.lengthis the Block Total Length, so the split consumed the whole block as header (len(frame.packet.header) == 348on a 348-octet block), and a block’s trailer (option list, repeated total length) means nolengthcould make it right. The fix is at the computation rather than the injection:PCAPNG.packetis 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. TeachingProtocolBase.__init__()to skip a protocol that had setpacketwas rejected: it healsframe.info.packetwhileframe.packetstill reports the whole block as header. The override is a plainproperty, not thecached_propertyit replaces: the inherited one caches because it reads the stream, where this one only walks buffers already filled, and caching it would let a secondunpackon 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.packetgoes fromb''to hundreds of octets,frame.packet.headerfrom the whole block to the pre-payload prefix. It was also a wire-format defect:PCAPIOwritesvalue.packetafter each record header, so a dump declared hundreds of octets per record and delivered none, desynchronising any reader that walks byincl_len. It round-trips now. The uncaughtValueErroron an EOF-truncated PCAP-NG (#678) is untouched, and #685 (below) later removed the staleexamples/captures/pcapng.txtfrom the index (#646).an unpadded HTTP/2
DATA,HEADERSorPUSH_PROMISEframe parsed with its whole payload silently discarded. The payload length callbacks for those frames inpcapkit/protocols/schema/application/httpv2.pyput the conditional in the wrong place: a conditional binds looser than-, sopkt['__length__'] - pkt['pad_len'] if pkt['flags']['bit_3'] else 0groups as(pkt['__length__'] - pkt['pad_len']) if ... else 0, and theelsearm returned0(“read no octets”) where it meant to subtract0. Padding is rare in HTTP/2, so the broken arm was the common one, and nothing raised or warned:info.datawasb''. An unpaddedDATAframe declaringb'{"ok":true}\n'now parses as those twelve octets, and unpaddedHEADERSandPUSH_PROMISEkeep their header block fragment. Padded frames are unaffected, since(A - B) if T else 0andA - (B if T else 0)are bothA - Bfor truthyT; the padded arm has no off-by-one (#668 asked that be checked), becauseSchema.unpack()decrementspacket['__length__']by each field’s width, sopad_len’s own octet is already out by the time the payload field runs. The same lambda also governs packing: an unpaddedDATAframe carrying twelve octets packed to a nine-octet header whose length field declared 21, body absent, so pcapkit was emitting malformed HTTP/2; amake-> parse ->makecycle of a non-empty unpadded payload now closes byte-for-byte. The only other conditional of this shape,TSOption.remainderinpcapkit/protocols/schema/internet/ipv4.py, is correct: itselse 0is intentional becausets_dataconsumes the whole option data area in that arm. The new tests use non-zero padding rather than the zeros RFC 9113 Section 6.1 tells a sender to use, so failing to subtract it yields a different byte string. Ships labelled breaking: restoring a dropped payload moves the parse output of essentially every HTTP/2 capture, and constructed frames change byte-for-byte, from malformed to correct (#668).the schema half of every option-like registrar overwrote silently while the parser half of the same call warned.
EnumSchema.register()was a barecls.__enum__[code] = schema, and is the schema half of the public option registrars, so oneregister_ipv4_optioncall replacing a built-in named the parser it displaced and said nothing about the schema.EnumSchema.__init_subclass__()reaches the same registry without callingregister, so a subclass declared with acode=keyword (the documented way to add a schema) stayed silent too; both are guarded, and the declaration hook’s branches are folded into one loop. Presence is a faithful test only because_EnumRegistry.__missing__()returns a miss without recording it, so parsing an unknown code cannot make the next legitimate registration warn. The code-keyed parser registrars (onProtocolBase,Link,Internet,Frame,PCAPNG,TransportandSCTP) now name the displaced entry and its replacement, where they reported only that something was overwritten. Their firing condition is harmonised onto the narrower guard #681 gaveregister_protocol(#718, #726) rather than presence alone: that key is derived from the value it stores, whereas these take a caller-suppliedcode. Where a table is pre-seeded with an unresolvedModuleDescriptor, the guard’siscomparison measures the resolved incoming class against the descriptor, so re-registering the same class under its own pre-seeded code (Internet.register(TransType.TCP, TCP)) still warns, a false positive the harmonised guard does not close.ContextRegistry.register()already raises on a duplicate, and the reassembly and ESP registrars append to lists with no key, so none of them takes a guard.import pcapkitemits noRegistryWarning: none of its registry writes (R1CounterParameter’s second code, from #690, among them) lands on a key already present (#692).every EOF-truncated PCAP-NG file raised a bare
ValueErrorout ofExtractor, losing the whole extraction rather than the one truncated block. Block Total Length is cross-checked against its trailing copy and never against the file, so a last block declaring more than the file holds leftPCAPNG.read()seeking past the end, and the next block read measured a negative remainder thatio.RawIOBase.readrefused. The seek is now clamped to the octets_read_filengreturned, with aProtocolWarningnaming the overrun, and a tail under the twelve octets a block needs is the quietStreamEOFErrorthatExtractor.record_frames()already catches (deliberately not a clamp, which would fabricate a frame out of zero padding). Another route to the same shape is closed by a newnonnegative()helper, composed intobounded_optionandbounded_areaand applied to the unwrappedlength - Nspans and the__option_padding__fields:_TextField.__call__()buildsf'{length}s'unconditionally, so a negative became'-8s'andstruct.calcsizeraised an exception caught nowhere a frame loop looks. Both of #678’s failure families are gone; others remain:ValueError: N is not a valid BlockType(filed as #701) andMemoryErrorfrom #593’s 32-bit band. Bare exceptions inSystemdJournalExportBlock.post_process(), reachable from valid input, are fixed with it: the body’s NUL alignment padding was read as a field name (struct.error; a NUL-only line now ends the entry); a binary field’s declared length was unbounded against its entry (OverflowError; now aProtocolErrorthat keeps the block’s journal data as captured, with aProtocolWarning, #1406); and a non-UTF-8 name, key or value raisedUnicodeDecodeError(nowerrors='replace'and reported). The silent loss of every field after a binary one is filed as #704. The unclamped non-packet option areas keep the per-block framing assumption #676’s note describes. Also here, the last registrar with no overwrite guard:Option.register()now reports a displaced option schema as aRegistryWarningnaming every namespace thens='opt'fan-out displaced something in, tested by membership (subscripting the per-namespacedefaultdictwould insertUnknownOptionfor a code nobody registered). The condition is identity-based like the parser registrars and #681’s guard forregister_protocol(#718, #726); a namespace the call itself creates is exempt. Flooring the decryption-secrets payloads that read__length__whole was tried and reverted, sinceSchema.pack()leaves that key at-1for “unknown”. Ships labelled breaking: well-formed captures are byte-identical, but any truncated PCAP-NG now yields the frames before the cut where it raised, the last of which may carry zero-padded octets, and some short-file depths move fromProtocolError: PCAP-NG: unknown byte-order magictoStreamEOFError. The #683 drift #685 tracks is unchanged (#678).HIP’s
R1_COUNTER/R1_Counterparameter packed 12 octets where RFC 7401 Section 5.2.3 requires 16, andLOCATOR_SETdeclared itsLengthin 4-octet units where RFC 7401 Section 5.2.1’sLengthis a byte count – independent defects, fixed together because the evidence for either needed the other out of the way.R1CounterParameter’scounterwas aUInt32Fieldwhere the RFC states the width twice, “8 bytes” in the diagram and “a 64-bit unsigned integer” in prose; both versions whose packing reaches the class,R1_Counter(128) andR1_COUNTER(129), packed a 12-octet record at4 (mod 8). It is aUInt64Fieldnow and the record is 16 octets (#672).LocatorSetParameter’sLengthwassum(Locator.len), in the 4-octet units RFC 8046 Section 4 givesLocator Length; a set of n plain IPv6 locators declared4noctets against24npresent, andSchema.unpack()handed the nestedListFieldonly the declared octets, son = 2andn = 5parsed one truncated locator and left the rest unconsumed.HIP._make_param_locator_set()now sums8 + Locator.len * 4per locator (#679). Another defect in the same parameter’s padding, which #651 left alone because it cancelled this one, is fixed alongside: the nestedLocatorschemas share the parameter’s packet context and pack their ownlenover it, sopadding, evaluated after the list, saw 4 rather than the real length. A newlocator_set_len_callbacksnapshotsLengthunder a private key before any locator packs, andlocator_set_padding_lenreads that snapshot. Thecounterdefaulted to0and the width defect could not be told apart from it, so the fixture now overrides bothR1_COUNTERcodes withcounter=0xaabbccdd; new tests aretest_hip_r1_counter_width_unit.pyandtest_hip_locator_set_length_unit.py. Thehip-parameter/R1_CounterEXPECTED_FAILURESentry (code 128 registered no schema of its own, sinceR1CounterParameterdeclared onlycode=129) was deleted once that registry defect was fixed as #690.HIP_COPIESdropped to one as #689, once #672 and #679 left it routing around nothing.httpv1.HTTPraised a bare exception for any payload that is not an HTTP/1 message, at these sites: the header/body split inread, the header’s CRLF split and the start-line split in_read_http_header, anditem[1]on a field line with no colon.HTTP._guess_version()falls through onProtocolErroralone, so the HTTP/1 attempt aborted the guess and the HTTP/2 arm below was dead code for each of those inputs. Each site now raisesProtocolError, which each method’sRaises:section documented, and a test pins the invariant (nothing escapeshttpv1.HTTPthat is not aProtocolError), so any further bare-raising site would fail there. An obs-fold continuation line [RFC 9112 Section 5.2] is legal HTTP/1 and is now unfolded, the RFC’s remedy, rather than read as a field of its own (with no colon it raised thatIndexError; with one it parsed into a spurious extra field). Reaching the HTTP/2 arm exposed a payload under nine octets tohttpv2.HTTP, where it usually fails inside the schema with a barestruct.error(not aValueError,ProtocolErroror pcapkitStructError, so uncatchable);readconverts it and_guess_versionsuppresses it on the last arm only, since an arm that is not last hands the payload onward when it swallows. Separately,PayloadField.protocollooked astrup in__proto__as given while the registry is keyed on the upper-cased class name, so a name not already upper-case resolved toNoneand yieldedRaw; it now folds case and warns withRegistryWarningfor a genuinely unregistered name, and__init__assigns through that property instead of writing_protocolpast it (#787, #789).a systemd journal entry whose last field lacks the format’s mandatory trailing newline let the PCAP-NG block’s 32-bit alignment padding be read as part of that field’s value, with no exception or warning:
b'MESSAGE=hello'plus three NULs returnedMESSAGE == 'hello\x00\x00\x00'. The padding-only-line guard from #678 catches padding only on a line of its own, which needs the preceding field to have ended with a real newline; without onereadline()runs through the value into the padding. Reachable on text fields only (a binary field’s length-prefixed read is never in play, and a binary field name on such a line fails its own length-prefix read regardless). A field line with no terminating newline raisesProtocolError, so the block’s journal data is kept as captured, padding included, with aProtocolWarning(#794, #1406).examples/generators/pcapng.py’s_journal_entrycited a bare line number for the defect its alignment workaround avoids and mis-attributed the fix; it now names #678’snot line.strip(b'\x00')guard and the mechanism (#791).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) % 6check.HTTP.unpacknow rejects a non-empty buffer under nine octets before the schema layer runs, preservingStreamEOFErrorfor a genuinely exhausted implicit-length stream, andread’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 barestruct.error; that root cause is in the genericSchema.unpack()/FieldBase.lengthmachinery the schema modules share, out of scope and filed as #805, which is why_guess_version’s last arm keeps suppressingstruct.erroralongsideProtocolError(#802).SystemdJournalExportBlock.post_process()skipped a binary field’s one trailing newline with a bareentry_data.read(), which reads to EOF: the nextreadline()returnedb''and ended the entry, discarding every field behind the first binary one. The skip is bound toentry_data.read(1), and a value not followed by its newline raisesProtocolErrorrather than resynchronising at the wrong offset, so the block’s journal data is kept as captured, as onebytesitem, with aProtocolWarning(#1406). Without the fix,test_journal_fields_following_a_binary_field_are_not_discardedfails withMissingKeyError: 'SECOND'(#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 raisesProtocolError, so the block’s journal data is kept as captured, with aProtocolWarning(#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 wrotelengthone octet short (#723).a breaking change to
.get()-backed enum fields: TCP/UDP/SCTP’sPortEnumFieldand PCAP-NG’sOptionEnumFieldeach minted a member throughaenum.extend_enumfor every value no row or documented span covers (111 calls onhttp.pcap, all ephemeral ports). Each now peeks at what.get()would consult and call it only once the value resolves; a genuine miss getsEnumField._unregistered_member(), a real member of the same registry built through its storage base’s__new__, soisinstanceholds and the lookup table stops growing. Riding along: PCAP-NG option namespaces stop cross-contaminating: once a code was minted underopt,OptionType.get()mergedoptover the requested namespace and most other namespaces answeredopt_unknownfor it; each now gets its own<ns>_unknown. Breaking because #575 asked for byte-identical output across the sample captures, as #420 and #427 did, and this deliberately misses that bar: some sample captures change, entirely from the new<unassigned>rendering plus the PLIST escaping above; normalising both leaves them byte-identical. So that a resolved unassigned member still pickles,_unregistered_memberinstalls a__reduce_ex__rebuilding an equivalent unregistered member (#575).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 realSETTINGSframe raisedProtocolError: unknown HTTP version. It now identifies before it parses: a preface is recognised as HTTP/2 outright, skipped rather than fed tohttpv2, and counted as header soinfostays byte-identical to reading the following frame alone (otherwise the injectedpacket=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 raisesProtocolError: HTTP/2: connection preface with no frame; a bare mid-streamSETTINGSframe, anUpgrade: h2crequest and non-HTTP input are unchanged, the frame and the request left undecidable on purpose (Upgrade: h2cneeds per-connection state a single payload cannot carry, and a mid-stream frame has no safe heuristic, since atype <= 9guess misfires on binary HTTP/1 bodies). Protochain over the sample captures is unchanged (#800).TCP.__proto__boundhttpv1.HTTPdirectly 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 genericpcapkit.protocols.application.http.protocol.HTTPproxy, whose identification became positive only once #800 and #814 landed, which is why this waited.udp.pyalready bound the proxy for both ports; this removes the asymmetry its docstring anddocs/source/contributing/pep.rstdocumented as an open request. Over the sample captures every HTTP/1.1 frame keeps its chain, and the genuine HTTP/2 frames inoptions-transport.pcapchangeEthernet:IPv4:TCP:RawtoEthernet:IPv4:TCP:HTTP/2. Not labelled breaking, but not free:TCP:80/8080traffic that is neither valid HTTP/1 nor preface-carrying is now exposed to the fall-through arm, where the directhttpv1binding left itRawregardless; a caller depending on that is the one who would notice (#682).a breaking change to more bespoke registries: step 2 of #860, ahead of
AppType(#874, above), bringingStatusCode,ReturnCode,ResponseKind,GroupingInformation,OptionType,FEATCode,CommandandMethodontoEnumRegistryand applying #775’s mint/unmint ruling to theirget()as well as_missing_.StatusCode,ReturnCode,ResponseKind,GroupingInformationandOptionTypeconvert 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, andStatusCode/ReturnCode’s hand-writtenget(), still on the retireddefault == -1convention, is replaced by the base’s (no caller relied on the old form).OptionTypekeeps its ownget()for its multi-namespace dispatch, and both its int/namespace andstrpaths 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,CommandandMethodmint the literal wire value as its own name rather than any placeholder; per the owner’s ruling thatget()lacks the information to create new members (Commandneedsfeat/desc/type/confandMethodneedssafe/idempotent, which a bare wire string lacks), both_missing_and each class’s ownget()(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 evaluatingCommand’s rows at import time, the import-time-mutation shape #861 removed fromFilterType; pinned count-agnostically so a future IANA update cannot fail a correct regeneration. Untouched, per rulings:CommandTypestaysIntFlag(realA|Pcomposites in the generated data), andTransportProtocol’sauto()renumbering andAppTypeare #874’s.pcapkit/protocols/schema/misc/pcapng.py’sOptionEnumField.post_processdocstring is corrected: it still bypassesOptionType.get()directly, now forpickleround-trip safety (#860 already stops the minting that used to be the reason), since neither ofOptionType’s paths gets both pickling and correct rendering right at once (#869).registeronProtocolBase,Frame,PCAPNG,SCTP,Link,Internetand the concreteTransportsubclasses (TCP,UDP) now raisesRegistryErrorfor a non-class argument, where it raised a bareTypeError(“issubclass() arg 1 must be a class”) from insideabc. Each guard gains an explicitisinstance(protocol, type)test ahead of itsissubclass, after theModuleDescriptoris unwrapped, so a descriptor naming a non-class attribute is rejected too. Every suchRaises:clause already promisedRegistryErrorifprotocol“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 raisesRegistryErroras before.Transport.register()onTransportitself still raisesUnsupportedCallfirst; only its subclasses reach the guard. Not breaking for exception handling:RegistryErrorsubclassesTypeErrorthroughBaseError, soexcept TypeErrorstill 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, becauseBaseErroris loud by default: the non-class path now emits pcapkit’s oneCRITICALlog record, and outside devmode it installssys.excepthookandthreading.excepthook, neither of which the bareTypeErrorfromabcdid. 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) (#1026).PCAPNG’sns_dnsname,ns_dnsIP4addrandns_dnsIP6addrscope guards, on both the read and the make side, now say the option “must be in Name Resolution Block”, which is the block they check; they used to name the systemd(1) Journal Export Block, with a literal:manpage:role in the text.register_record’sRegistryWarninglikewise now says “name resolution record already registered”. The make-sidens_dnsIP4addr/ns_dnsIP6addrmessages also drop the lowercasens_dnsip4addr/ns_dnsip6addrspelling for theOptionTypemember name. Other guards named the wrong option too:_make_option_if_rxspeed’s scope and duplicate guards now say[if_rxspeed]rather than[if_txspeed], and_read_option_epb_queue’s length guard says[epb_queue]rather than[epb_packetid]. Only message text changes; the exception and warning types are unchanged (#1038).httpv1.HTTPrefused every HTTP/1 message with no header fields, which [RFC 9112 Section 2.1] allows (*( field-line CRLF )):b'GET / HTTP/1.0\r\n\r\n'raisedProtocolError: HTTP: invalid format, because_read_http_headertreated a header with no CRLF as malformed. SoHTTPv1(method='GET', uri='/', http_version='1.1')failed to read back the bytes its ownmakebuilt, andHTTP._guess_version()answeredunknown HTTP version, since_test_start_linemirrored the same check. A CRLF-less header is now a start line with zero fields, in the parser and the predicate alike. That check was also what refused the HTTP/2 connection preface, whose header is exactlyPRI * HTTP/2.0; it is now refused by name instead, to the same message (#1041).httpv1.HTTP.from_datacould not rebuild an HTTP/1 message whose body is empty:HTTP.from_data(info)forb'GET / HTTP/1.1\r\nHost: a\r\n\r\n'raisedTypeError: can't concat NoneType to bytes.readrecords an empty body asNone,_make_datapassed it through, andmakeconcatenated it._make_datanow handsmakeb''for aNonebody;info.bodyis stillNone, so readers see no change.httpv2is unaffected: its empty-payload frames already round-trip (#1050).IPv6._read_ip_hextetsliced the traffic class fromhex[0:2]and the flow label fromhex[2:], both including the version nibble, so6abcdef0read as(6, 0x6a, 0xbcdef0); it now reads(6, 0xab, 0xcdef0). The parser reads these fields through the schema, so only a direct call was affected (#1094).the Mobility Header Message ID option wrote a zero fraction for every timestamp, and
MesgIDOption.post_process()ignored the fraction on read._make_opt_mesg_idnow writesround(frac * 2**32), clamped to 32 bits, and the reader addsfraction / 2**32seconds, sotimestampkeeps sub-second precision (#1093).HIP parameter defects.
HOST_IDpacked DI-Type and DI-Length as a 4-octetBitFieldwhere RFC 7401 section 5.2.9 has one 16-bit word, so every built record carried two surplus octets and read back the wrong algorithm (#1118); with it,make_samples.pybuilds every HIP parameter.HIP_CIPHERwarned at six Cipher IDs, which section 5.2.8 allows; it now warns only above six and keeps the first six, as a recipient must. The maker is unchanged (#1092). And the HIPv2-onlyDH_GROUP_LIST,HIP_CIPHER,HIT_SUITE_LISTandTRANSPORT_FORMAT_LISTwere built and parsed in a HIPv1 packet; at version 1 the reader and the maker now raiseProtocolError, asR1_CounterandHIP_TRANSFORMalready do on a version mismatch (#1133).the PCAP
Headercould not rebuild itself._make_magic’s explicit-endian branch ignorednanosecond(#1096), and_make_datareturned amagic_numberthatmake()does not take – one of the keys the #617 entry above reports – sofrom_datadropped it with anUnknownFieldWarningand rebuilt every header with the host-defaultd4c3b2a1magic._make_datanow returnsbyteorderandnanosecond, and every byte order and timestamp resolution rebuilds unchanged.Header._read_protosalso read four octets whateversizeasked for (#1125), and read them little-endian in a big-endian file; it now follows the file’s byte order (#2312). And_make_magichonouredlilendianandbigendianonly as a pair, soHeader(bigendian=True)wroted4c3b2a1on a little-endian host; either flag alone now picks the byte order and overridesbyteorder, and equal flags still raiseEndianError(#1144).TCP’s NS flag was written by
make()but never parsed: it had been commented out ofFlags, the reader and_make_data, with no reason recorded. Per the owner’s ruling it is exposed rather than removed, soinfo.flags.nsexists andfrom_datakeeps the bit (#1098).from_dataround trips.Frame._make_data()returnedts_srcandL2TPv2._make_data()prio, the other keys the #617 entry reports, sofrom_datadiscarded both with anUnknownFieldWarningand an L2TPv2 priority bit was lost; they are nowts_secandpriority(#1097). AndProtocolBase.from_data()took no keywords, soFrame.from_dataandPCAPNG.from_data, which neednum/headerandnum/sct/ctx, could not be called at all. Extra keywords now reach the construction, overriding_make_data’s, under the construction-keyword check, so a typo raisesUnsupportedCall(#1126).HTTP.from_dataalways raisedinvalid HTTP version: 0, looking up aversionkey neither versioned data class has; it now dispatches on the data’s class. HTTP/2’s_make_dataalso returned the payloadlength, whichProtocolBasetakes as the parse length, so even a direct call failed as shorter than the 9-octet frame header;makenow computes it, and every HTTP frame in the sample captures rebuilds byte for byte (#1154).Quick-Start options did not round-trip. The IPv4, HOPOPT and IPv6-Opts readers took the function from
schema.func, which only a parsed schema carries, so a built option raisedAttributeError; HOPOPT and IPv6-Opts also sized the 8-octet option as 5, misreading the nonce with apacket length < 0warning (#1091). TCP’s Quick-Start Response reader read one octet past the option, and its maker produced a negative rate code below 40 kbps; such a rate now rounds down to zero, which RFC 4782 permits (#1119).Ethernet,ARPandPCAPNGpacked MAC addresses as ASCII hex text.Ethernet._make_mac_addrreturned the separator-stripped hex string andBytesField(length=6)truncated it, soEthernet()put303030303030on the wire for an all-zero address (#1123).ARP._make_addr_resolve,PCAPNG._make_mac_addrand_make_eui_addrdid the same; all now decode the hex to octets, andfrom_datarebuilds each byte for byte (#1134).TCP’s User Timeout maker wrote
length=3where RFC 5482 and the reader require 4, so every option it built failed to parse withProtocolError(#1120).a breaking change to the HTTP/2 frame
length: it counted the 9-octet frame header, where RFC 9113 section 4.1 counts the payload alone, somake()wrote00000efor a 5-octet PRIORITY frame andreadrejected RFC-conformant frames – and, since_read_http_prioritychecked for 9, pcapkit’s own PRIORITY frame. The length and every per-type check now exclude the header. Captures written to the old convention no longer parse; the PRIORITY, RST_STREAM, SETTINGS (with and without ACK), PING, WINDOW_UPDATE, DATA, PUSH_PROMISE and GOAWAY frames that Scapy’scontrib.http2packs now parse and rebuild byte for byte (#1121).a breaking change to
ARP.make():hlenandplenare derived from the packedshaandspawhen omitted, and an address that disagrees with its length field raisesProtocolErrorwhere it used to be truncated silently –ARP(ptype=IPv6, spa='::1')wroteplen=4and four octets of the address. A non-IPstrprotocol address packs as hex octets rather than ASCII text, as hardware addresses do since #1134, and non-hex input raisesProtocolError(#1139).HTTP/2
SETTINGSbuilt each setting pair as aSchemaFieldwith no length, so a well-formed frame warnedpacket length < 0twice per pair. The item now has its fixed 6-octet size, as TCP SACK and SCTP gap-ack items already do (#1140).HTTP._guess_version()took almost any binary for HTTP/2 on its fall-through: nine zero octets read as a DATA frame. The HTTP/2 trial now runs only on a header with a type RFC 9113 defines, no undefined flag, the reserved bit clear and a stream that type is sent on. Over 5,000 fuzzed zero-heavy buffers, HTTP/2 labels fell from 566 to 101, every one a legal DATA header. Explicitversion=2parsing is unchanged (#1141).a breaking change to
TCP.make()’s options:_make_tcp_optionsdropped theNOPandEOOLoptions it was given and padded each other option to 4 octets separately, sofrom_datarebuilt none of the segments inhttp.pcapwith their original header octets. Options are now emitted as given and padded once at the end – zero octets after a trailingEOOL, otherwiseNOPoptions and anEOOL– so a built header can be shorter than before; every TCP segment of the measured sample captures rebuilds its header octets, andoptions-*.pcapregenerate unchanged (#1151).a breaking change to
from_dataon a capture truncated by snaplen:IPv4,IPv6andUDPrebuilt their length field from the payload at hand, writing the recorded length, sobig_endian.pcapframe 3 came back as45000052where450004a2was on the wire._make_datanow passes the parsed length through tomake()’s newtotal_length(IPv4, UDP) orpayload_length(IPv6) keyword, which computes the length as before when omitted, soIPv4(payload=...)is unchanged (#1155).a breaking change to the layer chain of a capture cut inside a header: a next layer parsed from too few captured octets was zero-filled, and
from_datarebuilt the fabricated octets.IPv4.read()handed UDP the declared payload length when nothing past the IPv4 header was captured, sotest.pcapngframe 3 rebuilt with 8 invented UDP octets (#1167). Every_import_next_layernow goes through one guard: no captured octets givesNoPayload, and fewer than the next layer’s__length_hint__givesRawunless the parsed header fits anyway, as a 12-octet L2TPv2 ZLB does. A header-only IPv6 packet isIPv6rather thanIPv6:UDP:Raw, and frame 3 isEthernet:Raw, rebuilt byte for byte (#1170). The guard needed honest hints, soIPv6_Route(4),MH(6),HOPOPTandIPv6_Opts(2) now hint their 8-octet minimum header (#1173), and so does theIPv6_Extfallback, which hinted 2 (#1178).a breaking change to
L2TPv2, against RFC 2661 section 3.1.Typewas inverted; it is nowData = 0andControl = 1, regenerated inpcapkit.constfrom the fixedpcapkit.vendortable, so the same bytes parse to the otherflags.type, and a defaultmake()writes T=0 where it wrote T=1. A control message now getsLand a Length field automatically, and the offset pad, whichfrom_datazero-filled, is kept asinfo.padding– a new key ininfoand in dumped output (#1175).from_dataalso clearedLand dropped the Length field:_make_datareturned it aslength, whichProtocolBasetakes as the parse length.make()’slengthkeyword is nowtotal_length, written as given; a call still passinglengthlosesLsilently (#1172).make()also gainslength_flagandpadding, and raisesProtocolErrorfor atotal_lengthwithlength_flag=False.a breaking change to
IPv6.from_data: it dropped the extension-header chain, so a packet carrying a Routing header rebuilt 48 octets as 40. The chain is now kept in wire order under a hidden__exthdr__key, absent from dumped output (to_dictcarries it since #2014, below), and re-emitted ahead of the upper layer. Sample frames that rebuilt wrong now rebuild exactly, though some that had rebuilt with wrong bytes raisedAttributeErrorinstead (#1177), from older defects fixed with it:HIP.aliasread_infobeforereadhad set it, which also broke a standalone HIP parse andHIP.from_data(#1184), and the MPL option’s maker calledbit_length()on theIPv6Addressseed of an S=0 option. MPL inHOPOPTandIPv6_Optsnow takesSfrom the parsed option or a newseed_typekeyword, and a missing or oversized seed raisesProtocolError(#1185). With both fixed, every IPv6 frame of theoptions-*.pcapcaptures rebuilds byte for byte.a breaking change to the SMF_DPD option’s maker in
HOPOPTandIPv6_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-octetDEFAULTTagger ID rebuilt as IPv4. Both now come from the parsed option, or frommodeand a newtid_typekeyword. A Tagger ID that does not fit its type raisesProtocolError, so a 17-octet one does where it raisedFieldValueError, andtid=b''builds a NULL Tagger ID where it raisedValueError(#1190). An H-DPD option with an empty Hash Assist Value raisesProtocolErrorwhere it raised a bareIndexError(#1193).a
ForwardMatchFieldthat 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 aHOPOPTorIPv6_Optsheader was re-read from the padding and raised a bareTypeError; it now raisesFieldValueError, as CALIPSO and MPL do. A full read rewinds as before (#1195).HIP()with no parameters wrote Header Length 0, below RFC 7401 section 5.1’s minimum of 4 for the two HITs, so it failed to read back withProtocolError; it now writes 4.readnames a Header Length below 4 in a newinvalid header lengthmessage. Lengths 0 to 3 already raisedProtocolError, so only the message changes (#1189).the RPL option, type
0x23, raisedAttributeErrorinHOPOPTandIPv6_Opts(#1196, #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, #1776).a breaking change to the RPL option: it keeps its reserved flag bits on rebuild and reads RFC 6553 sub-TLVs (#1199, #1201).
make()rejected an enum member name given asstrunless a namespace was passed (#1210, #1227); raw items in amake()list build a packet, andstrmember names resolve per argument (#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 themake()keywords andinfokeys the rebuild needs, which is what breaks:ARP, InARP and DRARP, any Ethernet
hlen, and aptype/plenmismatch (#1224); L2TPv2 reserved flag bits, and a lonensornrinmake()(#1226); octets past the L2TPv2 and OSPF Length fields (#1455, #1460).SCTP chunk flags, reserved fields and final padding (#1253), octets after the last whole chunk (#1468, #1472), a last chunk short of its padding (#1474, #1477), a final parameter or cause short of its padding (#1479, #1481), and a Host Name with no NUL terminator (#1482, #1483).
TCP: MPTCP options (#1254); reserved bits, octets after EOOL and the UTO granularity (#1348); and a header length declared past the data, with IPv4 (#1411).
FTP and HTTP/1 lines, byte for byte (#1301), keeping the raw command case, HTTP/1 whitespace and field octets, with whole multi-line FTP replies parsed (#1347). HTTP/2 frames lose no octets (#1341).
OSPF keeps the parsed Packet Length, and NGAP refuses octets after the NGAP-PDU (#1304); OSPF cryptographic authentication rebuilds from its dict form (#1464, #1469).
IPv4 option areas, route slots, timestamps and reserved bits (#1344); the TS option’s slots beyond the pointer (#1349, #1390); TS and SEC options byte for byte (#1399).
IPv6 core headers (#1307);
HOPOPTandIPv6_Optsoption areas (#1310); a CALIPSO option’s trailing pad (#1403, #1407), and itsmake()writing a corrupt length on overflow (#1408, #1409); an extension header parsed withextension=True(#1446, #1449); RPL source routes (#1463); and a cut extension-header chain (#1476).octets past IPv4 Total Length and IPv6 Payload Length (#1209, #1431), and past the IPX Length field (#1433, #1435). IPX
from_dataraisedstruct.error, and AH wrote its reserved field back as zero (#1338).HIP: Header Length, an empty parameter list, the PUZZLE lifetime and reserved bits (#1342); Locator flags (#1365, #1367);
BitFieldnamespaces’ reserved bits (#1417, #1419); and the two fixed header bits (#1459, #1462).MH: zero-length PadN, reserved fields and BU/BA flag bits (#1324, #1345); the reserved bits of the
BitFieldflag namespaces (#1418, #1421); and the reserved octetsPaddingFieldsites capture (#1442).PCAP-NG: constructed and rebuilt blocks (#1339), lengths and padding (#1373), blocks and options as parsed (#1378), further follow-ups (#1412), malformed journal entries and short packet blocks (#1420), an SHB or IDB shorter than its fixed fields (#1422, #1425), and captured padding octets (#1441).
Schema.pack()keeps option padding,SchemaFieldspans and iterated keys (#1374); aSchemaField’s unread octets survivefrom_datarather than rebuilding zero-filled (#1380, #1397); a header cut inside a fixed-width field rebuilds as captured (#1465); and each non-finalSchemasubclass is finalised on its own state (#1498, #1499).from_datarebuilds the payload frominfo.to_dict()(#1447, #1452).building a PCAP frame with
snaplen0 wrote a record with no packet data (#1385, #1410); an out-of-rangeif_tzoneis kept as captured and everyfromtimestampfailure is caught (#1340, #1429).registration: the parser registrars warn only when the entry really changes (#1362, #1400); re-registering a stored
ModuleDescriptorkeeps it (#1364, #1415); restoring a registration puts back the shipped descriptor, including throughPCAPNG.register()(#1533, #1573); and unregistering a custom protocol removes its name from__proto__(#1505, #1530).fragments: an IPv6 extension header behind a first fragment is dissected as without it (#1539, #1540), and a later IPv4 or IPv6 fragment’s data is raw, not an upper-layer header (#1545, #1552, #1553, #1562).
IPv4: a Total Length of 0 is read as TSO, and one inside the header as bogus (#1547, #1554).
IPv6: a jumbogram’s payload is dissected up to its Jumbo Payload Length (#1434, #1440), a jumbogram RFC 2675 calls an error is rejected (#1687, #1713), and so is a header whose Version is not 6 (#1680, #1723).
the warnings of a next layer replaced by
Raware dropped (#1580, #1588).IPv6-Route’s data-type readers name the routing type in their
ProtocolError, where each message read[TypeNo <class 'type'>](#442), andHTTP(..., version=1)parses from the frame’s data rather than the drained file, where it raisedValueError(#447, #451).Schema.pack()accepts atuplefor aListField, as the data models declare, so HIP and MH options that hold one round-trip (#476, #480).a Mobility Header CGA extension reports its length with its 4-octet header, where every parsed one read 2 octets short (#512, #517, #528).
TCP’s SACK option enforces the
8n + 2length RFC 2018 gives it, as its docstring promised (#519, #535).IPv4.from_data()rebuilds a parsed datagram:Schema.pack()checks a payload againstProtocolBase, and option padding ends in anEOOLoption rather than a bare wire code (#506, #536).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, #537, #543).
the MPTCP option header reads
lengthfrom bit 8, per RFC 8684 Section 3, rather than bit 1 (#553, #558).Mobility Header enumerations defined inline from their RFCs (
FastBindingAcknowledgmentStatus,IPv6AddressPrefixCode,LocalizedRoutingStatus,LMAAddressCode) raiseEnumValueErrorfor an unassigned value rather than mintingUnassigned_N(#880, #889), and each inheritsgetand itsdefaultfromEnumLookuprather than overriding it (#935, #940).pcapkit.protocols.protocol.__all__listsProtocolbesideProtocolBase, sofrom pcapkit.protocols.protocol import *binds the module’s public class (#1770, #1778).the data and schema packages re-export what their submodules list in
__all__, under the prefixes their siblings use:pcapkit.protocols.data.linkexports the L2TP data model asL2TPandL2TP_Flags(#1797, #1806), and the Mobility Header models, the HOPOPT and IPv6-OptsSMFDPDOptionandQuickStartOption, PCAP-NG’sPacketBlockandPACK_*options and HTTP/2’sSettingsreach their packages and the aggregators (#1807, #1815).the MPL option’s
seedin the HOPOPT and IPv6-Opts schemas is annotatedint | IPv6Address, since an S=0 option holds the source address (#1840, #1866).a declared length above
sys.maxsize, such as a PCAPincl_lenof2**31 - 16on GraalPy or 32-bit CPython, raisedOverflowErrorfrom a read or seek. Every length-driven read inSchema.unpack(),Frame.read()andPCAPNG.read()is bounded atsys.maxsize, so such a claim gets the overrun handling it gets on 64-bit CPython (#1975, #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_lengthruns past the block keeps the declared length and warns, its secrets capped at what the block holds (#1980, #1983); an unknown, Custom or systemd journal export block the file truncates gets the “block truncated” warning the other block types give (#1984, #1988); and a malformed TLS or WireGuard key-log line warns withProtocolWarningand is left out ofentries, where it raisedFieldValueError(#1985, #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, #2001).capture timestamps convert without the platform’s
fromtimestamplimits. Windows’ C runtime covers only 1970 to 3000, so a PCAP-NG timestamp outside that range fell back to 1970 withinvalid timestamp, and the Mobility Header’s NTP and PMIPv6 timestamps had the same gap. PCAP, PCAP-NG and MH convert throughpcapkit.utilities.datetime.fromtimestamp(), which gives the stdlib’s result on Linux (#2099, #2111), and encode throughpcapkit.utilities.datetime.totimestamp(), since GraalPy’stimestamp()put a pre-1970 instant with a fraction one second late (#2134, #2136).ARP’s address makers refuse what they cannot pack: a
boolprotocol address raisesFieldValueErrorwhateverptypeis; a hardware address that is none ofstr,bytesandbytearray(aboolincluded), or a protocol address not packed as IPv4 or IPv6 that is none of those and not anipaddressaddress, raisesProtocolError, where both failed later with a bareTypeError. Abytearraypacks asbytes(#2166, #2182, #2256, #2257).Ethernet’s and PCAP-NG’s MAC address makers, and PCAP-NG’s EUI address maker, raise
ProtocolErrorfor an address that is none ofstr,bytesandbytearray(aboolincluded), where they raised a bareTypeError(#2222, #2232).the Quick-Start rate parses as the
intthe IPv4, HOPOPT, IPv6-Opts and TCP data models declare, where it was afloatsuch as80.0, and a TCP Fast Open cookie request, the option of length 2, parses withcookieasb''rather thanNone(#2167, #2185).PCAP-NG’s
ns_dnsIP6addroption and IPv6 name resolution record build with their defaults,2001:4860:4860::8888and::1, where IPv4 defaults made them raiseFieldValueError(#2168, #2179).message text: the
epb_flagsandpack_flagslength errors sayexpected 4, the length they check, rather thanexpected 8(#2168, #2179), and the IPv4 TS option’s unknown-flag warning and the Mobility Header’s prefix error spelltimestampandmobile(#2171, #2189).IPX dissects its payload from a registry of its own,
IPX.__proto__, keyed by IPX Packet Type, where it looked the Packet Type up as an IP protocol number, so a type 17 (NCP) payload parsed asIPX:UDPand type 4 asIPX:IPv4. No IPX-native protocol ships, so the payload isRawunless one is registered (#2169, #2187).HIP’s
ECHO_RESPONSE_UNSIGNEDmaker builds anEchoResponseUnsignedParameterschema, where it built the request’s; the octets were already right (#2170, #2188).a breaking change to PCAP-NG’s Interface Description Block maker:
snaplendefaults to 0, which the PCAP-NG specification reads as no limit, where its default of2**64 - 1was truncated to0xFFFFFFFFby the 32-bit SnapLen field and the madeinfodisagreed with the octets. A block made withoutsnaplenwrites 0 there, and asnaplenoutside0to0xFFFFFFFFraisesProtocolErrorwhere it was truncated silently (#2278).HIP parameters read and rebuilt against the wrong definition.
TRANSACTION_IDandOVERLAY_IDkeep the identifier’s wire width on rebuild, through a newid_lenfield, where the length was sized from the integer’s ownbit_lengthand the leading zero octets were lost (#2271); theHIP_TRANSFORMmaker raisesProtocolErrorfor the HIPv2 parameter its own reader already rejects (#2275);LOCATOR_SET’s maker derives the locatortypefrom whetherspiwas given, where anspi=locator was left at type 0, a combination the locator selector itself rejects, and an explicittypethat disagrees raises naming both values (#2276);REG_INFO,REG_REQUESTandREG_RESPONSElifetimes decode as the2**((v-64)/8)seconds RFC 8003 Section 4.1 defines, with a zero octet its own case, where the octet was read as raw seconds, and their makers take either a duration or the raw exponent (#2281);OVERLAY_TTL’sttlis the hop count RFC 6079 defines rather than atimedelta(#2282); andCERT’scert_groupis a plainint, since RFC 8002 Section 2 defines no registry for it, where it was decoded through the DH Group registry (#2283).HOST_ID’s maker prefixes its errors in the houseHIPv{version}: [ParamNo {code}]form (#2299). The data-model and signature changes are breaking (#2400).TCP’s Multipath TCP options: an unknown subtype sizes its declared
lengthfrom what the schema writes, so one built withdata=b''declares a Length of 3, the header octets it always packs, rather than 2 (#2272); and a parsed subtype schema no longer carries anoptionattribute it does not declare and no maker mirrors – the declaredkindalready holdsMultipath_TCP, and the octets were identical either way (#2293, #2396).PCAP-NG’s Decryption Secrets Block maker builds from a
bytessecrets_data, itsb''default included, by reading those octets through the same secrets schema the parse side selects, where every secrets type raised because the readers expect a parsed schema (#2273, #2346).HTTP/2’s
makedefaultsframeto the frame type’s own constructor rather than to ab''packed verbatim with Length 0, soPRIORITY,RST_STREAM,PUSH_PROMISE,PING,GOAWAYandWINDOW_UPDATEbuild a frame that reads back, whereHTTP(type=..., sid=...)raised; an explicitb''still packs raw, and the default is byte-identical toframe={}.settings=Nonebuilds the parameterless SETTINGS frame RFC 9113 Section 6.5 permits rather than raising (#2274, #2395).IPv4’s Timestamp option builds from its own default:
timestamp=Nonegives a Timestamp-only option withcountsempty slots, where the default raisedProtocolErrorbecause the maker took only a list or a dict, and any other value is reported with itsrepr(#2277, #2347).a short read raises
StructErrorfor every size, not only for the sizes with astructformat code: theint.from_bytespath checks the length it got, where it returned a smaller integer.quietstill returns the partial value (#2279, #2349).the Mobility Header’s LMAA option carries the code its address family requires: the code defaults to that family’s own and an explicit one that disagrees raises
ProtocolError, whereaddress='10.0.0.1'packed the IPv6 code anyway (RFC 5949 Section 6.2.2). A captured mismatch still rebuilds byte for byte (#2280, #2350).a base class’s
makekeywords stop reaching a subclass whose ownmakeignores them. A newProtocolBase.__own_make_keywords__marker, read per class like the__keywords__opt-out, keeps a base’s parameters out of the allowlist once a subclass overridesmake, andIPv6_Extsets it, soIPv6_Route(len=99)is refused where it built the same octets asIPv6_Route()and a misspelling was steered to the dead keyword (#2286, #2358).TCP’s, UDP’s and SCTP’s
makeraiseProtocolErrorfor aboolport, whereUDP(srcport=True)packed port 1 andTCP(dstport=False)port 0 (#2287, #2359).SCTP’s Supported Address Types and Missing Mandatory Parameter makers raise
ProtocolErrorfor a parameter type that is neither aParametermember nor anint, aboolincluded, whereTruebecame parameter type 1,FalseraisedEnumValueError, and astror afloatwas stored unchecked (#2288, #2363).Loopback.makeraisesProtocolErrorfor an address family that is not anint, from the same check as its range test, whereTruepacked family 1,'2'andNoneraisedTypeErrorand2.0raisedAttributeError(#2289, #2364).FTP’s
makeraisesProtocolErrorfor a command that is none ofstr,bytesandbytearray, aboolincluded, where its fallback readcmmd.value, so anint, aboolor afloatraised a bareAttributeErrorwhile any object that happened to carry a.valuewas accepted unchecked. Abytearraybuilds asbytes(#2290, #2365)._make_pack, and_make_indexpacking through it, raiseStructErrorfor a size with nostructformat code, as thestructpath does, where a value out of range, a non-integer or a bad size leakedOverflowError,AttributeErrororValueError(#2291, #2367).a binary read at the end of the stream raises
StructError, as an unpack does, where an exhausted or short stream raisedTypeErrorout oford()(#2292, #2368).IPv4’s SEC option warns about the bit it observed: the field termination indicator RFC 1108 puts in the final protection authority octet is reported as set, with its value, where the message said the indicator was not set (#2295, #2372).
a maker that refuses a caller’s argument names the argument and quotes the value, rather than reusing the reader’s
invalid formattext, which says nothing about which argument was wrong: SCTP’s chunk-flag, empty-data, hostname-termination and reserved-length guards, the Mobility Header’s data-type, option-length, empty-data and Pad1 guards, TCP’sMP_CAPABLEoptional-field guard, and HIP’s and PCAP-NG’s fallback branches (#2296, #2405), then HOPOPT’s and IPv6-Opts’ Pad1 warning and HTTP/2’s and PCAP-NG’smakedispatchers (#2406, #2407). The readers still sayinvalid format, which is what a parse found.ESP’s errors read in the house
ESP: <description>: ... {value!r}form: the messages raised by the cipher and integrity lookups, byesp.protocol.SecurityAssociation’s SPI, destination, AEAD, ICV, key-splitting, decryption and encryption checks, by the context registrar and bymakeall carry the prefix and quote the offending value, where none carried anESP:prefix and most printed the value bare (#2297, #2366).HTTP’s version, data, start-line and settings errors carry the
HTTP:prefix and the rejected value, as inHTTP: invalid version: 3andHTTP/2: [Type 4] invalid settings: 42, where the prefixes were missing or malformed, some were capitalised with a full stop, and some left the value out (#2298, #2373).the capture guards read in that form too. The guards that reject a file’s magic number or byte order carry the
PCAP:andPCAP-NG:prefixes, and PCAP’s magic-number guard says what it read (#2300); and PCAP-NG’s option guards drop their trailing full stop, with the block-placement guard naming the block type through theBlockTyperegistry rather than printing the raw number_typeholds while a block is being made (#2301, #2398).IPv6_Ext’s version gate readsinvalid version: must be 6, not {version!r}, in the house form and quoting the rejected value, where its bespoke tail readonly valid for IPv6, got version=4– naming the version it requires in prose rather than as the number it compares against (#2302, #2397).a trial parse kept as
Rawbecause it raised no longer replays its warnings: the holding context honours itsdiscardon an exception exit too, and the next-layer reader discards a raising trial’s warnings underbeholder, so an Ethernet frame cut inside its IPv4 header stops warningpacket length < 0for a layer it does not have. Outsidebeholderthe error reaches the caller and the warnings are still reported, and the Raw’s error names the cut (#2308, #2339).the declarations that described a value the code never produces now match it, so a type checker and the rendered API agree with what is stored: CALIPSO’s
cmpt_bitmapis the tuple of ints its reader stores, the SMF hash-based DPD option declares its own schema rather than its sibling’s, anRRorSSRroutethat is always built is notOptional,NoPayloadnarrowsprotochaintoNone, ARP’s address maker admits thebytearrayits body handles, and the PCAP-NG flags stub and an undeclaredentry_rawfollow. HIP’sOverlayTTLParameterandViaRVSParameterjoin their modules’__all__and the package re-exports those lists require. Parsing and rebuilding are unchanged (#2317, #2377).HIP’s
parameters=[(code, data)]form rebuilds a parsed parameter through its maker positionally, so the parameters whose maker keyword is spelled differently from the data model’s own field name –suite_idagainstsuites,group_idagainstgroup,reg_typeagainstreg_requestand its siblings, and the nested locator keys – keep their values, where they fell back to the maker’s default or raisedAttributeError:HIP_TRANSFORMrebuilt as0241000000000000, losing itssuite_id. The dict form, as inparameters=[(code, {'suites': [...]})], is unchanged (#2352, #2383).PCAP-NG settles the byte order before a block maker runs, the way
packdoes – the section’s, else the one the packet data declares, else the host’s – so an option or a record handed over as rawbytes, whose type code is read back out of those octets, builds in any block rather than raisingAttributeError: 'PCAPNG' object has no attribute '_byte'. A Section Header Block still overrides it from its own Byte-Order Magic, and__packet__=Nonedegrades to no packet data rather than raisingTypeError, asSchema.pack()already treated it (#2353, #2369).a PCAP-NG block made from raw
bytesis read back off the octetspackhas just written, as a reader of the file would, rather than off the schema that carries them, so a Decryption Secrets Block built that way parses and rebuilds byte for byte, where the read looked for the block’s fields on the schema holding the octets and raisedAttributeError. Octets that are no block raiseStreamEOFErrororProtocolError(#2354, #2370).a
strenumeration name resolves where the namespace could not be inferred from the argument’s own default. HOPOPT’s and IPv6-Opts’seed_typeandtid_typeand HTTP/1’sstatusdefault toNone, so a name such as'SEEDID_16_BIT_UNSIGNED_INTEGER'raisedProtocolNotImplementedwhile the member andintforms of the same call worked (#2356, #2393); and HIP’s list-argument makers, where the name is an item of a list parameter rather than a bare argument, resolve too, with the enumerations they need imported at runtime rather than only underTYPE_CHECKING(#2394, #2401).a PCAP-NG block made without an explicit timestamp reads its interface’s resolution from the same lookup the maker already makes for the scale, so the Enhanced Packet Block, the obsolete Packet Block and the Interface Statistics Block default their timestamp where they raised
AttributeError: '_info', having no parsed data model to resolve the interface through yet (#2362, #2382).
pcapkit.toolkit¶
Added¶
util/pyshark_encap_map.py, a generator that regeneratesENCAP_TYPE_TO_LINKTYPEandFILTER_NAME_TO_LINKTYPEin place insidepcapkit/toolkit/pyshark.py, so the tables #850 hand-built stop being hand-maintained. It lives underutil/rather thanpcapkit/vendor/becauseVendor’s base class is hardwired to an HTTP fetch and a generated enum-class file, where this generator’s source of truth is the localtshark/editcapbinaries. It guards against its worst failure mode: a naiveLinkType(dlt)cannot detect an unmapped DLT, because_missing_mints a placeholder rather than raising, so the generator snapshots every knownLinkTypevalue first and callsLinkType(dlt)only once a value is confirmed a member. The real-tsharksweep is gated on a newHAS_WIRESHARKflag, registered intests/_dependency_gates.py’sNON_DISTRIBUTION_FLAGSnext toHAS_PROC_FD, for things pip cannot install. The sweep arithmetic is version-pinned (editcap -Taccepts 226 encapsulations on Wireshark 4.6.9, 224 on 4.2.2, CI’s Ubuntu noble), so count assertions run only under exactly the measured version, with a separateVersionIndependentInvariantTestsclass for what holds regardless. The only change topcapkit/toolkit/pyshark.pyitself is theIPMB_LINUXentry becomingI2C_LINUX, the canonical name #848 gave value 209 – the same member either way (#853).tcp_segmentand its result type are public inpcapkit.toolkit.pcap(#1570, #1579).
Fixed¶
the engine adapters, which were quietly wrong rather than loud. The
dpkttoolkit split TCP and IPv4 headers at their fixed struct size instead of their real length, so option octets overwrote payload in the sequence-indexed reassembly buffer; it also read anipv6_frag.nhthatdpktdoes not have, and passed fragment offsets unscaled (#351, #370, #385, #395).scapynever loaded its layer registry, so that engine did not dissect at all (#409), and its IPv4 fragment offset reached reassembly unscaled (#483, #484). The IPv6 adapters disagreed about whether the 8-octet Fragment header belongs toihl,headerandtl; per RFC 8200 section 4.5 it belongs to none, and they now agree (#415, #424).pypcapfile’sIP.src/.dstare dotted-decimal ASCII text in actypes.c_char_p(perpypcapfile0.12.0’sip.py), not packed 4-byte values, soipaddress.IPv4Address(ipv4.src)raisedAddressValueErrorandipv4_header()’sstruct.pack('...II', ipv4.src, ...)raisedstruct.erroron every call. A single_parse_ipv4_address()chokepoint replaces the separate call sites, also accepting packedbytesor anint. The same engine’sIP.opt/.payloadcarry hex-ASCII rather than raw bytes once decoding stops short of the transport layer; a new_maybe_unhex()covers the sites that read them. Notbreaking: nothing on this path worked on 3.10/3.11, so correct behaviour cannot regress a working caller (#743).pypcapfile’s_read_a_packet(layers=0)hexlifies a whole frame into ASCII text and stops, but_decode()fed that intoEthernet(packet.packet, layers=1), whose__init__unpacks the header withstruct.unpack, so every field came out garbage without raising. Fixed withbinascii.unhexlify(packet.packet)ahead of the decoder call, in preference to decoding eagerly atlayers>0inside the lazy generator, which would lose_decode()’s per-frameAttributeWarningfallback. Link-layer dispatch resolves from the header’sll_typeand is unaffected. Landed with #743: standalone, this turns a non-crashing extraction into a crashing one, sinceExtractor.run()catches onlyEOFError,StopIterationandKeyboardInterruptand theAddressValueError/struct.error#743 fixes escape until both land (#746).a breaking change to pyshark link-type resolution in
tcp_traceflow: an unrecognised encapsulation or filter name now raisesMissingKeyErrorinstead of substituting a plausible DLT.pcapkit/toolkit/pyshark.pyresolved a frame’s link type frompacket.layers[0].layer_name, a Wireshark display-filter name, with a blanketEnum_LinkType.get(name.upper())fallback. One filter name serves several encapsulations, so that fallback returned wrong DLTs silently (nullfor aDLT_LOOPcapture,101(RAW) forrawip6). Re-keyed ontoframe.encap_type, which is distinct per encapsulation, and the fallback is gone. Both tables were measured rather than transcribed (Wireshark’s source is not on the build host): each entry round-tripped througheditcap -F pcap -T <encap>and read back withtshark.ENCAP_TYPE_TO_LINKTYPEgrows to cover every measured encapsulation whose DLT has aLinkTypemember, andFILTER_NAME_TO_LINKTYPEgrows from #838’s hand-built entries to every root filter name, pseudo-protocol roots aside, that resolved to a singleLinkTypemember across the encapsulations this host’s Wireshark can write.pcapkit/toolkit/pyshark.pyreaches full coverage under new tests, which fail without the fix; live pyshark parsing stays unexercised, sincepyshark.FileCapturedoes not run on Python 3.14 (#850).pypcapfile’stcp_reassemblysetlastone past the segment.TCP_Packet.lastis inclusive, and the other adapters setseqnum + len(payload) - 1, as this one now does too (#1100).scapy’s
packet2dictdumped nested packets as{}and mutated its input (#1287); the dpkt reassembly adapters build their data from the record’s own octets rather than dpkt’s re-serialised bytes, and leave the packet alone (#1297, #1335).the TCP adapters: pyshark accepts tshark’s
True/FalseTCP flags (#1514, #1527); a TCP segment whose header the snapshot cut is traced and reassembled (#1518, #1556); dpkt reads SYN, FIN and RST whatever the NS bit (#1618, #1621); and the scapy adapters stop at the IP datagram’s declared length (#1584, #1589).timestamps: dpkt and pypcapfile hand over the correctly rounded timestamp (#1668, #1690), and a copied
DecimalTimestampkeepsorig_lenunder_pydecimal(#1647, #1653).the third-party engines are best effort: the code each adapter carried only to match the default engine is reverted (#1718, #1721, #1724, #1727, closing #1708 through #1743). What each engine leaves out, such as TCP behind a tunnel or an IPv6 extension header, is documented rather than warned about. The pypcapfile engine keeps its warning, once per capture, that it leaves TCP over IPv6 out (#1513, #1535).
the pcap-ct, PyPCAP, pypcapfile, pyshark and scapy adapters lead the messages below with their engine’s name, read off the engine class’s own
__engine_name__rather than hand-typed, so a rename underpcapkit/foundation/engines/cannot leave an adapter spelling the old one: the pcap-ct and PyPCAP reassembly refusals, pypcapfile’s address errors, its IPv6UnsupportedCalland its TCP-over-IPv6 warning, pyshark’s boolean-field, timestamp and missing-layer messages, and scapy’s import-time warning, which quotes the installable name, all read in the house<prefix>: <lowercase description>: ... {value!r}form. The dpkt adapter and the adapters’ModuleNotFoundraises keep the messages they had (#2304, #2399).the IPv4 and IPv6 reassembly adapters take the first layer of that IP version, from the outside in, that is actually a fragment – IPv4 with MF set or a non-zero offset, IPv6 with a Fragment header – so a fragmented inner datagram in an IP-in-IP tunnel is reassembled on its own header, where it was reassembled whole under the tunnel’s header, or dropped when the tunnel set DF or carried no Fragment header of its own. With no fragmented layer they take the outermost, as before. Applied to the default engine’s PCAP and PCAP-NG adapters and to the scapy and dpkt ones, which picked the outermost too (#2307, #2338).
pcapkit.utilities¶
Added¶
pcapkit.utilities.loggingas 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, andensure_output(). Each module that logs now does so under its own__name__, so a consumer can silencepcapkit.foundation.registrywhile keepingpcapkit.foundation.extraction(#384).pcapkit.utilities.config, one module for every tunable limit, each read once from aPCAPKIT_*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 withConfigWarningand falls back to the default;-1lifts a limit. The homes these limits had in the prereleases keep no re-export (#1726, #1743).pcapkit.utilities.datetime, holdingfromtimestamp()andtotimestamp(), the timestamp conversions of the capture-timestamp entry underpcapkit.protocols(#2111, #2136).
Changed¶
pcapkitno longer configures logging at import. It installs aNullHandlerand sets no level, so verbosity is inherited from the application instead of being seized by whichever library was imported second; the old stderr handler stays as thePCAPKIT_DEVMODEopt-in. Consequences: the previous behaviour isconfigure(logging.INFO, stream=sys.stderr); the registry and extractorinfocalls becamedebug, so those messages are invisible even atINFO; and the handler is no longerlogger.handlers[0].verbose=output stays on stdout and is not logging (#384).each warning is reported once per channel, and
pcapkitno longer inserts asimplefilter('ignore', ...)at the front of the process-globalwarnings.filters(#362–#364, #390). The application’s own filter therefore wins, which is the point and also the sharp edge: under-W error, or pytest’sfilterwarnings = error, a pcapkit warning that used to be suppressed will raise. Suppress them deliberately withwarnings.filterwarnings('ignore', category=BaseWarning).quiet=Truenow means no record at any level and no longer setssys.tracebacklimit, and thepcapkit.utilities.warnings.DEVMODEre-export is gone; its home ispcapkit.utilities.logging.pcapkit.utilities.compatexportspathlib,ModuleNotFoundError,collections.abc.Collectionandfunctools.cached_property()as the standard library has them: their fallbacks for interpreters below 3.8 are removed with that floor (#2051, #2053).
Fixed¶
further gaps the #514 keyword audit turned up, none covered by a test:
StreamEOFError’s docstring did not say that@preparealways raises it withquiet=True(the end-of-stream conventionStructErrorfollows via its owneof=True), so nothing pinned that silence against regression; andregister_extractor_engine’s real keyword,name, was not itself under test, only its corrected docstring, so a rename could put the two out of step again (#577).hold_warningsis inpcapkit.utilities.warnings.__all__(#1602, #1603).the default threading excepthook is the fallback before Python 3.10, which has no
threading.__excepthook__(#1691, #1701).stacklevel()returns a level relative to its caller, aswarnings.warnandloggingcount it, so a warning or aBaseErrorlog record names the caller’s frame at any stack depth rather than a frame insideloggingor pcapkit (#397).re-executing
pcapkit.utilities.loggingunderPCAPKIT_DEVMODEkeeps one stderr handler, where eachimportlib.reloadadded another (#391).a loud
BaseErrorno longer sets the process-globalsys.tracebacklimit, which truncated every later traceback, pcapkit’s or not; lazily installedsys.excepthookandthreading.excepthookkeep its output terse instead (#983).an import-time warning names the importing module on GraalPy too, where the importer’s stack level counted the bootstrap frames GraalPy’s
warnings.warnskips, attributing the warning to<sys>:0(#1963, #1996).
pcapkit.vendor¶
Fixed¶
re.subsites underpcapkit/vendor/passedre.MULTILINEas the fourth positional argument, which iscount, notflags: substitution capped at 8 matches, and aDeprecationWarningon Python 3.13+ (a futureTypeError). Found by an AST sweep rather than grep, since most put the flag on a continuation line. The flag moves toflags=at every site, indefault.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.pyandtcp/option.py. Behaviour preserving: the shared pattern (r'\r*\n') has no MULTILINE-sensitive anchor, so noconst/regeneration was needed (#796).mainwas red (#866) withRecursionErrorintests/const/test_const_enum_lookup.py,tests/const/test_const_enum_no_mint.pyandtests/protocols/misc/test_pcapng_unit.py.pcapkit/vendor/pcapng/record_type.pyandsecrets_type.pyrendered a_missing_body whose first line did not return:cls._unregistered_member(value, 'Unassigned')followed byreturn cls(value). That was harmless while the first line wasextend_enum(...), which registers the member socls(value)found it; #861 (above) replaced it with_unregistered_member, which deliberately does not register, socls(value)missed and re-entered_missing_. #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”, whichVendor.process()’s shape legitimately breaks. The guard sees only some crawlers; the rest are covered behaviourally. Closes #866 (#867).python -m pcapkit.vendor <target>could not fail:run()swallowed every crawler exception as a filterableVendorRuntimeWarningandmain()always returned0, 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 returns1if any raised, writing the failing target’s qualified name and exceptionreprto stderr unconditionally viaprint()rather than the filterable warning. Per-target snapshot and restore: a new_snapshot_and_restorecontext manager backs up a target’s const file withtempfile.mkstempbefore it runs (resolving the destination throughVendor._dest_path(), now aclassmethodinpcapkit/vendor/default.pyso it can be called beforeVendor.__init__fetches, renders or writes), restores the backup viaos.replaceon 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, sinceVendor.__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 underbash -ewhenpcapkit-vendorexits non-zero. The exit code is captured around that one command, so targets that succeeded are still sorted byisort, 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 always0. A per-targetgit checkout -- <path>restore step was dropped: with snapshot/restore, a failed target’s const file is never touched. Closes #872 (#873)._snapshot_and_restore(#873) carried an unpinned invariant and inaccurate prose and lint, none of it changing behaviour. 1.except BaseException:in_snapshot_and_restoreis pinned: a test asserts that aKeyboardInterruptstill restores the file and propagates. 2. A false docstring clause is corrected: a crawler whose_dest_pathis an instance-method-only override fails the class call_snapshot_and_restoremakes but not the instance call inside__init__, so that target runs unprotected and succeeds silently. 3. The# pylint: disable=no-else-raisepragma is kept, because it is needed: under theMakefile’s flags, which CI runs, pylint reports R1720 on theelse:. A--disable=all --enable=no-else-raiserun reports nothing there, since enabling the one message does not re-enable the check on atry/except/elsethe way--enable=Rdoes, so such a run cannot show the pragma unneeded. Theelse:is deliberate: it makes the backup cleanup unreachable from theexcept. Left out of scope: an untestedos.close(fd)leak (harmless, sincecopy2reopens the path) and the semantically identical unindented form of the sameelse:. Closes #875 (#876).TLSKeyLabeltracked the Mozilla NSS key-log wiki, butdraft-ietf-opsawg-pcapng-06section 4.7 now points at[I-D.ietf-tls-keylogfile](draft-05), published as RFC 9850, “The SSLKEYLOGFILE Format for TLS”, whose section 4.2 creates the IANA “TLS SSLKEYLOGFILE Labels” registry. Against it,TLSKeyLabelwas missingECH_SECRETandECH_CONFIG(RFC 9850 sections 2.3 and 4.2), soTLSKeyLabel('ECH_SECRET')raisedValueErroron a legitimate Decryption Secrets Block; both are added,ECH_SECRETkeeping the file’s# nosec B105bandit convention for*_SECRETmembers.RSAhas no row in RFC 9850’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 Formatcomment is repointed at RFC 9850, andTLSKeyLabel’s docstring cites it. A new test pins the member set against RFC 9850 plus theRSAexception. Closes #882 (#883).both spellings of an IANA citation render as
:rfc:roles in the generatedpcapkit.constdocstrings (#1664, #1673).MAX_RETRYandCI_MODEstay inpcapkit.vendor.default, read fromPCAPKIT_VENDOR_RETRYandPCAPKIT_CI_MODE; a bad value warns withConfigWarningand falls back, where 1.4.x crashed on a non-integer and read0as1(#1745).the IPX socket crawler no longer scrapes Wikipedia, whose page dropped the socket table, so a regeneration stops failing on it (#507, #511).
the crawlers send a descriptive
User-Agentbuilt from the package metadata, which Wikipedia requires, and the PCAP-NG crawlers read draft-03, the one revision that reproduces their constant files (#518, #524).pcapkit.vendorandpcapkit.vendor.mhlistMH_HandoverInitiateStatusin__all__, where each repeatedMH_HandoverACKStatusin its place, so a barepcapkit-vendorrun reaches its crawler (#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, #542).
each crawler derives its
const/path from thepcapkit.vendorroot, so a crawler at any depth, such asvendor/reg/apptype/tcp.py, writes to the right file (#741).pcapkit.vendor.ipxnames itssys.pathtemporary_pathrather thanpath, which read as a public name of the package, and the public-API test coverspcapkit.vendor, which it used to skip (#1772, #1779).the generated const comments render as written: a URL reference followed by another bracket no longer autolinks into it, a leading enumerator such as
(S)is escaped, and a hyphenated word is no longer split across lines (#1846, #1847, #1871).without the
vendorextra,pcapkit-vendorandpython -m pcapkit.vendorexit 1 with one line naming the missing packages andpypcapkit[vendor], where they died with a traceback, and--helpand--versionstill work. Importing a crawler then raisesModuleNotFoundError(#1850, #1875).the ESP cipher and integrity crawlers strip the brackets off IANA’s
[UNSPECIFIED]“no document” cell before testing it against the unknown placeholder, sopcapkit.const.esp.integrityno longer reads#: AUTH_DES_MAC [UNSPECIFIED] (DEPRECATED ...)forAUTH_DES_MACandAUTH_KPDK_MD5(#2294, #2371).some dict-based crawlers’
process(self, data)ignored thedataargument and read the module-levelDATAliteral instead, inipv4/{qs_function,tos_del,tos_ecn,tos_pre,tos_rel,tos_thr,ts_flag}.py,ipv6/{option_action,qs_function,smf_dpd_mode}.py,mh/{binding_error,lla_code}.pyandpcapng/{filter_type,hash_algorithm,secrets_type,verdict_type}.py. Harmless today, since every real call site passesrequest()’s ownDATA-returning value, sodata is DATAalways held in practice – but a caller passing different data, such as a test fixture or a futureLINK-based fetch, got the module’s table back regardless, with no error. The dict-based crawlers are hand-written rather than generated, so the fix needs noconst/regeneration (#2429, #2430).
Project infrastructure¶
Added¶
an end-to-end test tier (#376), sample-capture generators so a fresh clone can rebuild every fixture (#340), a Dockerised engine benchmark covering every supported Python version (#410), and registry round-trip coverage that records the entries which cannot close the cycle rather than skipping them (#440, #504).
coverage for the untested half of the #431 accommodation: a TCP or IPv4 option whose declared length asks for more data than the capture holds, pinning that it still parses with the short read zero-padded rather than rejected. The test #431 left covers only an option area with no data behind it; a candidate fix for #554 turned the untested half into an unwrapped
FieldValueErrorwhile the rest of the suite stayed green (#571, #572).CITATION.cff, citation metadata in Citation File Format 1.2.0, which GitHub renders as the “Cite this repository” button and which citation managers and dependency inventories read directly. It is the machine-readable half of the attribution BSD-3-Clause already asks for. Validated withcffconvert --validateand against the published 1.2.0 schema;doiandorcidare omitted rather than invented, since neither exists for this project and both are checked formats, so a wrong value would still validate. The licence is unchanged (BSD-3-Clause, noNOTICE). Alongside it the copyright line moves from2018-2023to2018-2026(LICENSEwas its only occurrence;docs/source/conf.pyderives its own from the current year), and a straysafter the closingDAMAGE.of the licence text, present since the Mozilla-to-BSD relicence, is removed so the wording matches canonical BSD-3-Clause (#615).examples/generators/endian.py, and the byte-order tests that read what it writes. There was no big-endian.pcapin the repository, which is why #605 survived its own code review: the fix to a misspelled key left the corrected path as untested as the broken one. The generator writesbig_endian.pcap(magica1 b2 c3 d4),big_endian_nanosecond.pcap(a1 b2 3c 4d, the first fixture to take that branch of the magic-number table) andlittle_endian.pcap(d4 c3 b2 a1), carrying the same records in each, so tests can assert that byte order makes no difference to what is read rather than only matching numbers written in a test. Frame 3 is captured short (1200 octets on the wire, 96-octetsnaplen), soincl_lenandorig_lendiffer and cannot both be satisfied by one byte-swapped value.test_frame_endian_runtime.pydrives each file throughextract()and derives expectations by walking its record chain withstruct; a unit-tier case intest_header_frame_unit.pybuilds a big-endian capture in memory, so the fixture-free selection CI runs on every push catches it too. Without the fix, each fails (the fixture-backed ones by thatValueError, the in-memory one byAssertionError: 3106905 != 1500000000) while the little-endian twin passes with and without it, showing the records themselves are not the variable (#605).local type stubs under
typings/for dpkt, pyshark,pcapand pypcapfile, declaring what pcapkit uses of each, so mypy checks the engines against them.mypy.inireads them throughmypy_path, and neither the sdist nor the wheel ships them (#1878, #1882).a glossary page for the terms with several meanings – registry, schema, data model, info and default engine – each sense linked to the page that explains it (#1949, #1955).
the engine benchmark on GitHub Actions:
benchmark.yml, weekly and on dispatch, measures every engine on the runner’s own interpreters throughexamples/benchmark/native.shand posts the report to the CI reports discussions (#2125, #2128). Apcapengine whose module cannot load is an explained row rather than a fatal error (#2129, #2131), and the report is reST with footnotes, so it pastes unedited ontodocs/source/engines.rst(#2142), the page the engine comparison and prerequisites move to fromindex.rst, carrying the first run’s figures (f9de043d6,d7aa41535). The tables had first been regenerated with Python 3.15 measured (#2109).a win-64 Conda package (#2060). Conda Update builds and smoke-tests it (#2067), reading the environment without
os.environb(#2120, #2122) and conda-forge’s CFEP-27 site-packages layout (#2126, #2127); then it publishes from Conda Update and Create Release, and a release is complete only once the platforms its tagged commit lists, win-64 among them, are on Anaconda (#2138). A manual Conda Update run builds only (c5081fbfe) unless dispatched onmainwithpublish, which forces the bump that ships a release’s missing Anaconda files (#2150), and Create Release’s warning about an unfinished release names that remedy when Anaconda files are what is missing (#2140, #2151).
Changed¶
a breaking change to
requires-python: it is>=3.8, so pip refuses an older interpreter at install, as “requires a different Python”. The sources use 3.8 syntax, and thebpc-*backport tools that converted it for 3.6 and 3.7 are no longer maintained, so the conversion goes: thebpc-*requirements,refactor()and thesetup.pybuild commands that ran it. The 3.7 KTLO (keep the lights on) legs go with it, andcoverage-comment.ymllists no install failure as by design any more, which closes #2040 (#2051, #2053).setup.pyis gone, andpyproject.tomlis the only build configuration. With the 3.8 floor it was a Python 2 guard and a baresetup(): the wheel’s files are identical without it,pip install -e .goes through PEP 660, pip still refuses 3.7 as “requires a different Python”, and on 2.7 it fails on thesetuptoolsbuild requirement before it would reachsetup.py.examples/benchmark/Dockerfilestops copying it, and a test fails shouldsetup.pyreturn or aCOPYthere name a file the repository does not track. Dependabot now fetchespyproject.tomlinsetup.py’s place and passes it through unstubbed; the staticreadmestays, as it does not depend on what a stub would supply (#2061, #2066).renames with no compatibility alias left behind:
examples/sampleandexamples/samples– one letter apart, holding different things – are nowexamples/capturesandexamples/generators.the README is a landing page now, and Markdown rather than reStructuredText.
README.rstbecame a much shorterREADME.md, 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 indocs/source/index.rst, so they are linked rather than restated; the blocks that existed nowhere else moved: Testing is nowdocs/source/contributing/testing.rst, and thepipenvandmake setupblock joined the Installation section ofdocs/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.pyreadREADME.mdand declaredtext/markdown(pyproject.tomldeclares thereadmestatically since #1994), andMANIFEST.ingainsinclude README.md, which is belt-and-braces rather than load-bearing: setuptools’sdistships aREADME.mdregardless. Of theincludelines onlyCHANGELOG.mdis load-bearing, measured in the #631 entry below.examples/benchmark/DockerfilecopiesREADME.md, while theREADME.mdthatexamples/benchmark/benchmark.pynames is the benchmark suite’s own (#619).CODE_OF_CONDUCT.mdmoves from Contributor Covenant 1.4 to 3.0, at the owner’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. Some things needed deciding. 3.0’s[NOTEplaceholders are filled or removed: the reporting channel namesjarryshaw@icloud.com(the contact 1.4 carried andSECURITY.mduses 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, this repository having no moderators. The enforcement 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 permanentversion/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,LICENSEuntouched). The ladder renders as nested list items, checked against GitHub’s Markdown API; #613 had to repair the 1.4 file when a stray list marker collapsed it into one nested item (#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. Module-level directives for pure implementation detail (esp._resolve,esp._CRYPTO,ngap._convert,ngap._revert,ngap._PYCRATE,ngap._PDU_LOCK,pypcapfile._NamedStream) were dropped, whileautoattributedirectives were added for class-private state that is contract, among themExtractor._flag_f, thePyPCAPandPCAP_CTengines’_backend(the only third-party engines with one),TraceFlow’s internal fields,FieldBase/Fieldinternals, the built-inPCAPengine’s_gbhdr,_vinfo,_nnsecand_dlink(shared withPyPCAP,PCAP_CTandPyPCAPFile) andPCAPNG’s_ctx/_ctx_list. The runtime definitions of_missing_(underpcapkit.const, and inline inpcapkit.protocols) gained one write-up in place of a directive per class, a new “Unrecognised Values” section indocs/source/pcapkit/const/index.rstcross-referenced fromregistry.rst(conf.pyalready excludes_missing_from everyautoclass).CONTRIBUTING.mdgained the rule as a named section. The new Sphinx warning, a bareTypenewly rendered byTraceFlow._foutio’s directive, is #709. No line underpcapkit/changed (#684).examples/captures/out.json,out.plist,out.txtandpcapng.txtare no longer tracked in git; they are build output, not fixtures, and a tracked rendering with no reader goes stale silently.pcapng.txtis exactly that: it still recordedpacket -> NILfor the Enhanced Packet Blocks ofdhcp.pcapnglong after #683 gave those blocks their captured octets back. Rather than regenerate it again and leave the drift free to recur, these files are removed from the index and folded intoexamples/captures/’s blanket.gitignorerule; the genuine inputsin.pcapanddhcp.pcapngstay tracked.examples/legacy_smoke/Makefileand itsREADME.mdnow describe regenerating these reports viamake fixtures, andtests/project/test_capture_tracking.pypins the invariant (#685).tests/project/test_isort_clean.pygated onisortwith atry/except ImportErrorinsidesetUpClass, whichtests/_dependency_gates.pycannot see (_gates_of()walks a decorator list, never a function body), andisortwas in nopyproject.tomlextra, so the file reportedOK (skipped=1)on every CI leg and the dependency-gate guard could not tell. It now gates on a module-levelHAS_ISORTand a class-level@unittest.skipUnlesssogated_scopes()counts it,MODULE_PROVIDERSmapsisort, andisortjoins thetestextra rather than being excluded (lint.ymlinstalls none, so unlikemypythere is no existing check to defer to). No workflow changed, every pytest job already building on.[test];gated_scopes()gains this scope anddependency_gate_gaps()is unchanged. Another instance of the hazard #745 exists to stop (#766).tests/vendor/test_vendor_reg_apptype_generator_unit.pygated onmypywith noMODULE_PROVIDERSentry, so_top_level_providers()died on a bareKeyError: 'mypy'once the gate became visible – a further instance of the hazard #745 exists to stop, like #766 above.MODULE_PROVIDERS['mypy'] = ('mypy',)fixes the lookup, and the bareMODULE_PROVIDERS[...]subscripts route through that function, which raises anAssertionErrornaming the module, key and table on any future gap.mypyis deliberately not added to apyproject.tomlextra: no extra carries it, it is aPipfiledev-dependency thatlint.ymlinstalls and already runs whole-package, andlint.ymlruns no pytest job forpytest_jobs()to see, so the fix is aDEPENDENCY_GATE_EXCLUSIONS['HAS_MYPY']entry ontest,engine-testsandgate.dependency_gate_gaps()gains the new(flag, job)pairs, each expected (#779).engine='pyshark'had no CI job driving it through a realtshark-parsed capture.pypcap-parityinstallspysharkbut itsapt-get installnamed notsharkpackage, soPyShark.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-paritynow installstsharkwith the debconf pre-seedengine-testscarries, and the test proves which engine ran (noEngineWarning,extractor._exnam == 'pyshark', aPySharkinstance), as the PyPCAP parity test does. Onlytests/integration/test_engine_runtime.pyandtest_engine_parity.pychange; the length assertion is kept (#846).engine-testsran a Python-version leg per interpreter regardless of which engines that interpreter could install, sopyproject.toml’sPyPCAPFile = ["pypcapfile; python_version < '3.12'"]marker resolved to nothing on 3.12–3.14, yet theEngines Python 3.12/3.13/3.14legs 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:supportedcells install through the real extra and must pass;unsupportedcells (PyPCAPFileon 3.12–3.15,PySharkon 3.14/3.15) install by rawpip installto bypass the marker and assert the engine’s ownPYTHON_CEILINGdecline; andnot-installablecells (PyPCAPon 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 iscontinue-on-errorthroughout. Job names change shape (Engines Python XbecomesEngines 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).Ruleset
23497679’srequired_status_checksnames exact check contexts, and GitHub rulesets match literally with no wildcard, so the list needs hand-editing on every matrix change. The oldEngines Python <version>names in it are dead since #849; theCompat Python 3.10-3.14names stay live, emitted bypython-compatibility.yml, and are untouched. Adds a job,required-checks(contextRequired checks passed), depending on[test, integration, engine-tests, pypcap-parity]withif: always() && inputs.gate-only != trueand an explicit per-dependency check that fails loudly, naming the job, on anything butsuccess: a bareneeds: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’stest,integration,Enginesandpypcap-parityentries once the ruleset is updated separately, as a repository setting outside the change; theCompatnames are excluded from the replacement. Only.github/workflows/unit-tests.ymlchanges; no library or test code (#856).engine-testsruns one job per engine,Engines (<engine>), each looping over its interpreters in fresh virtual environments, in place of #849’s job per Python and engine.required-checksalso needschangesandproject-tests:project-testsmust pass on every diff, and a skipped leg passes only wherechangesclassifies the diff as docs-only (#1052, #1055), which may skiptest,integration,engine-testsandpypcap-parity, or as test-only, which may skipengine-testsandpypcap-parity(#2243, #2244).the prose the post-#719 defect wave added or touched, checked against the code; no code changes.
MesgIDOption.secondsin the Mobility Header schema said seconds since 1970 where it holds an RFC 1305 NTP count from 1900;HTTP._guess_version()comments predated the HTTP/2 gate; and the_parse_next_layernote miscounted the captured octets.Extractor._open_output()now says only the PCAP engine passes header fields,Extractor.run()and the PCAP-NG engine’srunlist theFormatErrorfor PCAP output from a PCAP-NG input, and the extraction and engine pages document the PCAP-output path (#1179, #1180, #1186, #1103).a breaking change to the
allextra: it carries the core addons only –cli,cryptoandNGAP. The third-party engines 1.4.x bundled in it (dpkt,scapyandpyshark) and the vendor crawler’srequestsandbeautifulsoup4are no longer in it; the engines keep their ownDPKT,ScapyandPySharkextras and the crawler itsvendorextra, andpypcapfilehas a newPyPCAPFileextra of its own. A newdevextra carries what CI needs (#910, #914).the
Scapyanddevextras requirescapy>=2.6.0(#1572, #1578), and thedocsextraSphinx>=7.3, the first release withversionremoved(#1743), and an exactsphinx-autodoc-typehintspin (#1856, #1877), 3.17.2 since the weekly dependency update (#1944). Thetestextra gainspsutil,markdown-it-pyandlxml, which the scripts underutil/use (#1894, #1896). Thecryptoandallextras capcryptographybelow 46 on PyPy older than 3.11, for which no later release has a Linux wheel (#2040, #2046).the READMEs of
examples/benchmarkandexamples/legacy_smokeare Markdown,README.mdrather thanREADME.rst, andMANIFEST.inships them throughrecursive-include examples *.md. Stale build leftovers go:setup.py’sbdist_wheeloverride, sincebuild_pyandinstallalready convert the tree the wheel archives, andMANIFEST.in’sprune test, which named no directory;make profileruns inexamples/legacy_smoke(#1773, #1780).MANIFEST.inalso drops the lines that matched nothing –global-include *.rst,include pcapkit/_externandexclude pcapkit/protocols/*/NotImplemented– and the source distribution and wheel are unchanged (#1794, #1813).the contributor pages state no counts that a test or the Python version updater pinned, and each pin checks the fact it stood for instead (#1811, #1817).
the workflows’ supply chain (#1968): every third-party action is pinned to a commit SHA that Dependabot updates by pull request (#1981); no third-party step runs after the deploy key is loaded, and every workflow declares its top-level permissions, the GitHub Pages build read-only beside a separate publishing job (#1970); and the cron jobs name the environments that hold their secrets (#1993).
pyproject.tomldeclares thereadmestatically, so Dependabot can build the editable install (#1994).CodeQL skips
tests/, where the recurring clear-text-logging alerts inpcapkit.foundation.extractionandpcapkit.corekit.iotraced to a test fixture’ssecretsnames;pcapkit/is still scanned by every query (#2130, #2139). The advisory linters drop their long-standing false positives with no behaviour change: vermin honours the# noverminmarkers and confirms the 3.8 floor, bandit reports nothing, and the unusedtype: ignorecomments go, from the vendor templates too (#2145, #2147).
Fixed¶
places where a documentation page contradicted the code (#413), ambiguous cross-references and autodoc signature failures (#416), and
Extractor’s documented exception plus phantom or staleArgs:labels (#501).both halves of what the
Changelog driftgate told an author, inutil/changelog_md.py.ResidualMarkupErrornamed the entry file but numbered its lines against the converted body, which rule 6 joins onto one line per block, so a reported number could not reach most of the file, and roles on separate source lines were all reported as one line. The conversion now carries a source map, so every complaint cites a line of the file the message names, one per construct, in the entry’s order (#588). Rule 2 accepted only the bare-number spelling of Sphinx’s:rfc:role, so RFC 6554 Section 3 fell past it and was reported as a role the rules do not cover; both spellings now convert, the anchor carried into the link target and the link text taken from Sphinx’s own (#592). RegeneratingCHANGELOG.mdis byte-identical over every committed entry: the fix changes what the gate says, not what it emits.examples/generators/options.py’sTCP_BASEspelled header fields with namesTCP.make()does not declare, so every option fixture inexamples/captures/options-tcp.pcapwas built from values the generator had not asked for.makeends in**kwargsand reads nothing out of it, so an undeclared keyword is accepted and discarded with no warning.'seq': 1wasseq_no, so every captured frame carried sequence number 0;'urgent_pointer'wasurgent(requested value and default both 0); and'ack_flag': Falsewasackwhile'ack': 0bound toackitself, the acknowledgement flag rather than the number (ack_no), so that pair was wrong both ways. Each is renamed to the parametermakedeclares, keeping the intended values.TCP(**TCP_BASE).info.seqwas 0 against a declared 1 and is now 1, andoptions-tcp.pcapchanges only in the low octet of each frame’s sequence number, with no case status orEXPECTED_FAILURESentry moving. The module’s other base mappings were audited: the rest pass nothing theirmakedoes not declare, and HIP’sextensionis declared byHIP.read(), this library’s documented way of forwarding a read-path keyword, not another instance (#602).util/bump_version.pyleftCITATION.cffnaming the previous release. Nothing else maintains that file, so every bump since #615 would have stranded theversionanddate-releasedthat 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 triggerscreate-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-versionand areferencesentry’s ownversionare anchored out at column zero), and the result is checked withcffconvert --validate. An absent file is reported on stderr and skipped rather than failing the vendor cron before itsgit commit, which would discard a whole registry crawl for a documentation file; a file with noversionfield raises before anything is written, since reporting success while rewriting nothing is the staleness being fixed. The script gains amain()guard (it ran the whole bump at import, hence no testable surface), and theimport pcapkitfallback in its version reader, which returned"1.5.0b4'\n"(closing quote and newline, whichpackagingrejects), 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).LICENSE’s copyright notice began the term at 2018, a year after the work it covers. The repository’s first commit isc57f7d0b7, dated 2017-11-07, so the notice understated the term and contradicteddocs/source/conf.py, whose Sphinx footer isf'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) introduced2018-2020when the BSD-3-Clause text replaced MPL 2.0, and hand bumps to2018-2022,2018-2023and2018-2026(#615) each copied2018forward. 2017 is restored rather than newly asserted: the initial MITLICENSEreadCopyright (c) 2017 Jarry Shaw, and the GPL v3 era carriedCopyright (C) 2017 Jarry Shawin its appendix; it was lost on 2018-12-08, when4f3e00d5frelicensed to Apache 2.0 underCopyright 2018 Jarry Shawand1c69341dcreplaced 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 singularCopyright (c) <year> <owner>. 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.pyis untouched (a self-maintaining footer showing a range is conventional and correct). TheBSD 3-Clause Licensetitle line and the comma in the notice follow choosealicense.com’s template, whose first line is that title and whose notice readsCopyright (c) [year], [fullname]. The licence body is unaltered: the change is confined to the copyright line, and theDAMAGE.that #615 repaired fromDAMAGE.sis still clean (#630).CITATION.cffshipped in no source distribution.MANIFEST.inhad anincludeeach forREADME.md,LICENSEandCHANGELOG.mdbut none for the citation file, and itsglobal-includepatterns (*.rst,*.py) could not match a.cff. Removing thoseincludelines and rebuilding showsREADME.mdandLICENSEshipping anyway (setuptools adds the latter fromlicense_files, recorded asLicense-File: LICENSEinPKG-INFO) whileCHANGELOG.mdvanishes, so onlyCHANGELOG.mdis 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 flagged the omission and #619 did not address it. It matters because #625 taughtutil/bump_version.pyto keep the file’sversionanddate-releasedin step, and a release exercising that path would publish an sdist omitting the artefact under test. It also letsRepositoryCitationTestsintests/project/test_bump_version.py(the gate #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 --sdistnow differs from before only by the archive entrypypcapkit-1.5.0b4/CITATION.cff, the shipped copy is byte-identical, andtwine check --strictpasses on both (#631).the release workflow published to PyPI and to Anaconda with nothing a human had to approve.
create-release.yml’spypijob hadenvironment: releasecommented out while theid-token: writefrom 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; thecondajob had noenvironment:at all. Neither needed a tag: the workflow also fires on completion ofVendor 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:githubongithub-release(the GitHub Release and thev*tag),tagonconda-tag(the commit pushed tomain),pypionpypi,condaonanaconda. One environment per credential rather than a sharedrelease, because approval is granted to an environment and not a job, so onereleaseapproved once would release both indexes together. PyPI has to be answerable alone: an accepted version number cannot be reused, andskip-existing: truemakes 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: anenvironment: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 thetags: v*trigger fails outright instead of pausing.github-pagesis the standing example, carrying no protection rule since 2021. The new test asserts the general rule, that no job running a publishing action or agit pushmay omit anenvironment:, so a publishing job added later without a gate fails rather than ships (#641).test comments in
tests/_tiers.py,tests/test_tier_guard.pyandtests/integration/_helpers.pystated an exact count of committed captures underexamples/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.pydrops its ordinal). Prose-only, verified withcoverage.parser.PythonParserthat no executable statement moved (#700).test_every_tracked_name_exists_and_matches_gitintests/test_tier_guard.pychecked 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. Siblingtest_capture_suggestions_are_captureshad the same gap. Fixed in #703; #703’s description names both #700 and #708. The first test now re-derives the expected set fromgit ls-files -zunderexamples/captures/, asserts equality against it, and checks each name withPath.is_file(); its sibling re-implementscommitted_capture_names()’s filter inline, which catches a wrong filter but still trustscommitted_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).pcapkit.all.__all__listedOSPF,RARPandDRARPon its Link Layer line after #719 moved them (#1124), and omitted names its packages export, among themESP,HTTPv1,HTTPv2,PCAPNGandEnumRegistry, sofrom pcapkit.all import *did not bind them.pcapkit.utilitiesstays unlisted deliberately, so generic helpers such aswarndo not arrive throughimport *(#1136).examples/generators/options.pywrote every HTTP/2 sample frame on stream 0, which RFC 9113 forbids forDATA,HEADERSand the other stream-level types, so those frames inoptions-transport.pcapdecoded asTCP:Raw._httpv2_buildnow putsSETTINGS,PINGandGOAWAYon stream 0 and every other type on stream 1, withPUSH_PROMISEpromising stream 2, and every HTTP/2 sample frame decodes asTCP:HTTP/2; no other generated capture changes (#1156).make_samples.pybuilds fixtures from the tree it runs in, not the shared checkout (#1343, #1388), and retries the Wireshark sample downloads on 429, 5xx and timeouts (#1658, #1659).examples/generators/options.py’s deadline discards aSIGALRMstill pending as it puts the old handler back, which on GraalPy could kill the process (#2090, #2091).the Conda Update workflow tags and uploads each bump exactly once (#1632, #1641), stays green when a merge races its push (#1649, #1669), and keeps the release’s name and notes when it uploads (#1657, #1678). Vendor Update commits the new modules a crawl adds (#1685, #1688), and runs
tests/constandtests/vendoron the regenerated tree first, keeping only the modules they pass with (#1853, #1879).package
__all__lists match what each package holds: several disagreed with it, andfrom pcapkit.protocols.data import *raisedAttributeErroron the unboundHOPOPT_QuickStartOption(#515, #527); the public attributes several packages left out are listed (#533, #544).the documentation build’s “more than one target found” warnings, fixed where they arise: an implicit reference to
dict,typeorsetno longer matches a registry member such asTCP.dict, an enum member’s docstring holding a URL no longer gains a spurious type field, and the hand-writtenTypeVardirectives take the bare name under their.. module::(#1775, #1790).the Mermaid diagrams render at their natural size, shrunk only to the page width, where each was fitted into a fixed box, and their
clicktargets are relative to the page, so they resolve under the site’s/PyPCAPKit/prefix (#1103, #1819).the API reference renders what the code declares.
Extractor._vfunckeeps its annotation (#1839, #1867), and the HTTP/2 frame schemas’ and the engine classes’ annotations resolve (#1857, #1870). Under the pinned sphinx-autodoc-typehints (#1856, #1877),conf.pyrenders anaenumbase as such, keeps a default as written (byteorder=sys.byteorderrather than the build host’s value), gives a class the parameters of the__init__its__new__hides and an enumeration(*values), keeps table ids unique, and stops the source index linking a missing page (#1858 to #1863, #1881). The generated const tables carry no version directives; each note is on the module’s hand-written page instead (#1884, #1895).cross-references resolve or render as literals. A role into a package with no inventory is a literal or an
rst_epiloglink (#1919, #1928, #1936, #1939, #1920, #1945); apcapmodule mention links the note on PyPCAP and pcap-ct (#1948, #1952); library-internal names link, and the classes and special methods they name are documented (#1940, #1946, #1947, #1953), the const registries’getoverrides among them (#1921, #1935); and aWarns:section’s warning classes link asRaises:entries do (#2094, #2096).conf.pyresolves a decorated function’s overloads and qualifies a third-party class whose bare name collides (#1917, #1918, #1929). Theversionchangeddirectives onInfoiteration andRawname1.3.1.post8, the release that shipped their change (#1933, #1934).the Conda release legs: Create Release’s Anaconda completeness check counts osx-arm64 (#1922, #1932); the
macos-26-intellegs install Miniforge, upload with theanacondaCLI where the upload action’s download returned 404, and build again past upstream conda and conda-build breakage (#2044, #2045, #2049, #2123, #2124); andutil/conda_vendor.py’s pin refresh warns when it cannot move a platform pin, or when a pinned release has lost a wheel (#2025, #2026, #2028, #2035).pcapkit-cli’s help strings say what each option does:-odescribes the output rather than the input, an extension is appended only under--auto-extension, and each format flag names the flags that take precedence over it (#1796, #1810).the Conda package: its vendored dependencies are the package’s
dependenciesand itsallextra, with their requirements, shipped with their data files rather than as.pyfiles only (#1814, #1821); theosx-64package is built on an Intel runner, where converting the arm64 build shipped arm64 binaries (#1822, #1825); and on Intel macOS it pinscryptography48.0.1, which has a wheel for that platform where the newer pin has none (#1827, #1828).util/conda_vendor.py --checkreports a package pinned differently under one marker (#1852, #1865); the platform pins move toconda/platform-pins.toml, each with a review-by date, and Conda Update moves them up (#1851, #1885); and the release’s conda leg installs none of pcapkit’s extras and smoke-tests the package before uploading it (#1854, #1855, #1868).a wheel built with the oldest setuptools
[build-system] requiresallowed, without build isolation, left outpcapkit/py.typed, so type checkers read the installed package as untyped. Nothing declares the file, and setuptools ships it unasked only from 69.0.0, so that is the floor now, inpyproject.tomland the Conda recipe alike (#2062, #2064).examples/legacy_smoke’stest_perfbenchmarks every engine its support table lists, writing each result to../captures/engines/<engine>.txtor skipping the engine with its reason, where it looked each benchmark up in a table that did not cover pypcap, pcap-ct or pypcapfile, so the first of those to pass preflight raisedKeyErrorand ended the run (#2305, #2375).