Conversation
…um's ValueError (#701) * `EnumField.post_process` resolved a wire value through its registry's constructor, so a value no member and no `_missing_` rule accounted for raised `aenum`'s own bare `ValueError`. That is neither one of `pcapkit.utilities.exceptions` nor an `EOFError`, so a caller could not tell it from a bug of its own and `Extractor.record_frames` did not catch it: one unassigned code cost the whole extraction. * It also made the "unknown" reader every format requires unreachable. PCAP-NG declares `UnknownBlock` as its block-type default and carries a `_read_block_unknown` for it, but the lookup failed several frames before the dispatch that would have chosen it. * An unassigned value now resolves to the same nameless pseudo-member the method already built for a field carrying no registry at all, built per value rather than grafted onto the registry, so no process-global registry grows. * A rejection carrying one of `pcapkit.utilities.exceptions` still propagates, so a registry that deliberately bounds itself is not overruled by the field layer. No registry's own guard changes; `BlockType(28)` and `BlockType.get(28)` raise exactly as before. Tested: 11 new tests in `tests/corekit/test_fields_numbers_unassigned_enum.py`, 5 of which fail on `origin/main`; `tests/corekit/`, `tests/const/`, `tests/protocols/schema/` and the option round-trip suite all green.
9154383 to
f0af610
Compare
|
Cross-review (Sonnet, read-only, briefed to falsify — this change was authored on Opus): good to go on the code, needs changes on the description. It falsified the blast-radius numbers — 112 call sites not 106, and 10 rejecting registries not 6, with |
|
GOOD TO GO Independent cross-review, read-only, on a different model from the author (author: Opus; this review: Claude Sonnet 5). Head reviewed: Note on process: partway through this review I was told #706's head had moved from 1. Fixes the reported case — CONFIRMED (re-run on 2. Blast radius bounded and understood — CONFIRMED, with an independently-rederived correction that matches the one already in the PR body. I built an AST-based scan of every 3. Width guards on the seven flag registries — CONFIRMED not weakened. 4. Exception selection — CONFIRMED, and the premise needs a small correction. The diff itself never chooses an exception class — it only re-raises (bare 5. 6. 7. Tests fail without the fix, aren't vacuous, no hidden subtest failures — CONFIRMED, on both 8. Scope — CONFIRMED. Two things from the coordinator's follow-up brief, answered directly:
Could not verify: exhaustive (100%-of-space) coverage of the four remaining 32/128-bit registries beyond |
Closes #701.
The defect
EnumField.post_processresolved a wire value through its registry's constructor:which raises for any value no member and no
_missing_rule accounts for. The raise isaenum's own bareValueError— not one ofpcapkit.utilities.exceptions, so a caller cannot tell it from a bug of its own, and not anEOFError, soExtractor.record_framesdoes not catch it. One unassigned code therefore cost the whole extraction.It also made the "unknown" reader every one of these formats requires unreachable. PCAP-NG declares
UnknownBlockas its block-type default and carries a whole_read_block_unknownfor it, but the lookup failed several frames before the dispatch that would have selected it, so the default only ever fired for theReserved_*rangesBlockType._missing_auto-extends. PCAP-NG repeats a block's total length at both ends precisely so a reader can skip a block type it does not recognise; that skip is what this restores.Reproduced on
9d7890db4The issue's repro, verbatim, with the three line numbers it cites confirmed against this branch's base (they had not gone stale):
schema.py:860→field.py:528→numbers.py:513→aenum/_enum.py:2276.What changed, and what deliberately did not
An unassigned value now resolves to the same nameless pseudo-member the method already built for a field carrying no registry at all, rather than to a new value shape. That makes the diff small, and it means the dump layer already renders the result — as
<unknown>::<unassigned> [28], throughpcapkit/dumpkit/common.py'srender_enum— and anint-keyed dispatch registry already looks it up by value like any declared member.It is built per value rather than grafted onto the registry with
aenum.extend_enum, for two measured reasons:ProtocolBase._lookup_registry's own docstring exists to avoid;enum.IntEnumregistry is then handled identically to anaenum.IntEnumone.No registry's own guard is touched.
BlockType(28)andBlockType.get(28)raise exactly as before — the fallback lives in the field, which knows the value arrived in a fixed-width wire field. And only a foreign rejection is absorbed: a registry rejecting a value with one ofpcapkit.utilities.exceptionspropagates unchanged, which is what keeps this from being an unconditionalexcept ValueError: pass.That distinction is for registries registered from outside
pcapkit.const. No generated guard raises an in-library error today and none should —tests/const/test_const_enum_builtin_parity.py::test_the_exception_is_not_an_in_library_onepins the bare, unloggedValueErrorbecause every generatedget()'sexcept ValueErrorfallback depends on it (#584, #647). Separately, no bit-flag registry is named as a plainEnumFieldnamespace anywhere in the package, measured over all 112 call sites, sopcapkit/const/tcp/flags.py's 16-bit guard (#702) is not in this layer's path at all.Blast radius, measured
112 plain
EnumField(...)call sites carry a namespace. 106 of them resolve statically to 73 distinct registries; the other 6 are alias arguments insidemh.py'sbr_code_selector/fb_code_selectorand cannot be resolved without evaluating the call.10 of the 73 reject a probed value inside their field's own value space, and are what this changes:
BlockTypepcapkit/protocols/schema/misc/pcapng.py:461CauseCode(SCTP)pcapkit/protocols/schema/transport/sctp.py:232EtherTypepcapkit/protocols/schema/link/arp.py:26FlowBindingType(MH)pcapkit/protocols/schema/internet/mh.py:3079HITSuite(HIP)pcapkit/protocols/schema/internet/hip.py:959MNIDSubtype(MH)pcapkit/protocols/schema/internet/mh.py:722Option(TCP)pcapkit/protocols/schema/transport/tcp.py:403Parameter(SCTP)pcapkit/protocols/schema/transport/sctp.py:427Transport(HIP)pcapkit/protocols/schema/internet/hip.py:1265UpdateNotificationReason(MH)pcapkit/protocols/schema/internet/mh.py:301363 of the 73 resolved every probed value already, and are unaffected.
No bit-flag registry is among the 73 — so
pcapkit/const/tcp/flags.py's 16-bit guard (tests: the nameless-enum sweep probes 65536 against a 16-bit flag registry, so main fails without showing red #702) is not in this layer's path at all. TCP's flag byte is decoded bit-by-bit throughBitField, never as a whole-byte enum.The probe is bounded, not exhaustive — a dense band over 0..4095, every gap between declared members, and the top of the space — because a full 16-bit sweep of fifty registries mints millions of pseudo-members through
extend_enumand takes over twenty minutes. So 10 is a lower bound. Two earlier passes of this census got it wrong in both directions and are worth recording: sampling only between declared members missed every registry whose lowest declared value is above zero (CauseCode,MNIDSubtype,Parameter), and using the field's byte width where the call site declares a narrowerbit_lengthwrongly implicatedPriorityLevel, whose real field isEnumField(length=1, bit_length=3)and can only ever present 0–7 — all eight declared. Verified: all 256 octets resolve to a realPriorityLevelmember through that field.The four
post_processoverrides —PortEnumFieldinudp.py/tcp.py/sctp.pyandOptionEnumFieldinpcapng.py— all callsuper(EnumField, self).post_process(...), which the MRO sends straight toNumberField.post_processand so skips this method entirely.AppTypeandOptionType, whose members carry extra per-namespace attributes a value-only fallback could not supply, are reached only through those, so they are outside the blast radius by construction.The
isinstance(x, <registry>)chains downstream of a plainEnumFieldwere checked one by one. The one that looked reachable —pcapkit/protocols/internet/hip.py:3799-3841'sisinstance(hi_curve, Enum_ECDSACurve)/ECDSALowCurve/EdDSACurvecascade, whichraise ProtocolErrorin itselse— is not: each of those three registries mintsUnassigned_%dfor every value in its 2-octet width, so this fallback never fires for them and the cascade keeps seeing real members (ECDSACurve(65535)→<ECDSACurve.Unassigned_65535: 65535>,isinstanceTrue).Why
breakingProtocol.analyzecatchesexcept Exceptionand downgrades the failing layer toRaw, so today an unassigned enum inside a next-layer protocol silently costs that whole layer. Measured on a one-frame PCAP whose Ethernet EtherType is1501— a real IANA gap:frame.protochainETHERNETEthernet:<unassigned>frame.payloadRawEtherneteth.type<<unknown>.<unassigned>: 1501>More information, not less — but it is a visible change in parse output for existing input, hence the label.
Evidence
tests/corekit/test_fields_numbers_unassigned_enum.py, 11 tests. 5 fail onorigin/main:The remaining 6 are guards that must pass in both directions — they are there to fail against a wrong fix, not against
main.The sweep is the blast radius in one line. It walks all four registries before asserting anything, so its failure names every one that regressed rather than aborting at the first: on
origin/mainit reportsEtherType (2 octets): 62555 of 65536andBlockType (2 octets): 65509 of 65536, whileTransTypeandOptionNumberresolve everything in both trees and are the regression guards inside it.assertEqual(summary, [])fails on any single escape; the[:3]slice caps only how many bad values are listed, and the count in the message is the true total.test_an_in_library_rejection_is_not_absorbedis the one that distinguishes this fix from the wrong one: against aexcept ValueError: passvariant of the same change it fails both subtests (SUBFAILED(namespace='StdlibBounded'),SUBFAILED(namespace='AenumBounded')), measured.Regressions — all green, exit 0:
EXPECTED_FAILURESdid not move. Imported (it cannot be grepped —**unpacking): 44 entries, unchanged. None records an enumValueError; the only entry whose text mentionspost_processisipv4-option/QS, aboutpcapkit/protocols/internet/ipv4.py:1178's schema-level hook. The round-trip suite passes, so no recorded gap started passing and nothing needed deleting.Coverage of
pcapkit/corekit/fields/numbers.py, viacoverage run -m pytest tests/corekit/:The file was already at 100%/100%, so the change is that four more statements and two more branches are covered, by eleven more tests (179 → 190) — not a percentage move.
Lint unchanged.
pylintreports only the pre-existingW1309at line 81;mypy's single[misc]finding aboutenum.IntEnum("<unknown>")is present verbatim onorigin/main(there at line 509, here at 583);verminreports the sameMinimum required versions: 3.8.All measurements were taken with
PYTHONSAFEPATH=1, the repo venv (CPython 3.14.7), andpcapkit.__file__asserted to this worktree.Not done here
post_processruns per field per packet, so a warning is a judgement about noise on a large capture that belongs to the owner rather than to this fix; the existing registry-less branch resolves silently too.pcapkit/const/tcp/flags.pyandtests/dumpkit/test_nameless_enum_rendering_unit.pyare untouched — that is tests: the nameless-enum sweep probes 65536 against a 16-bit flag registry, so main fails without showing red #702, in flight separately.networkfield is a plainEnumField(Enum_LinkType)parsed outsideanalyze()'s catch-all, butLinkType._missing_mintsUnassigned_*for every value in its width, so no link type reaches the raise. Worth recording as checked rather than left implied.Cross-review
Reviewed by a second agent on a different model (Sonnet; this change was authored on Opus), briefed to falsify rather than to bless, running read-only with no write access to this repository or to GitHub. Verdict: good to go on the code, and needs changes on this description — it independently falsified the blast-radius numbers above, which have been corrected here and re-derived from scratch. Concretely it found: the site count was 106 rather than 112; four rejecting registries were missing (
CauseCode,MNIDSubtype,FlowBindingType,UpdateNotificationReason, and a fifth,Parameter, turned up in the re-derivation); andPriorityLevelwas a false positive from ignoringbit_length. It reproduced the before/afterbreakingtable itself, confirmed the fourpost_processoverrides are skipped by instrumentingEnumField.post_processand observing zero calls, confirmedEnumSchema.registryis a non-insertingdefaultdictsubclass so no dispatch registry grows, and confirmed a JSON dump of a frame carrying the pseudo-member renders as"type": "<unknown>::<unassigned> [1501]". It also noted the#648reference overstated the connection — that issue's guard is forname is None, which a member named<unassigned>never hits — and the docstring now says so precisely. It could not sweep the full 16/32-bit space of every registry, which is why the count above is stated as a lower bound.