Repository navigation
interface: dispatch follow_tcp_stream on the engine, and fix the dpkt crash - #402
Conversation
… crash `follow_tcp_stream` chose its reassembly adapter with `extraction.engine == 'dpkt'`, but `Extractor.engine` returns the `Engine` *instance*, so both branches were dead code, `fallback` was always true, and `pcapkit.toolkit.pcap`'s adapter was used against frames from whichever engine actually ran. The two `# type: ignore[comparison-overlap]` comments were mypy reporting exactly this and being silenced; both are gone, and mypy is clean on the file without them. It was only "clean" before because the always-false comparison made the `count=` call statically unreachable. Dispatch is now on the engine *type*. Chosen over `.name` because for a third-party engine registered as `class Foo(Engine, name='foo')` the registry key and the `.name` property diverge, and because a third-party engine subclassing a built-in then routes to the right adapter. An engine matching none of the built-ins warns and returns `()` rather than silently using the wrong one, which is the failure this issue is about. `fallback` became `pass_count`, set per adapter rather than per branch: the pcapkit-native adapters read the frame number off the dissected frame and take no `count`, while the third-party ones have no intrinsic number and must be passed `count=index`. The old code had those backwards. Second defect, and the issue's own diagnosis had it wrong -- I filed that issue, so the correction is mine. The dpkt `AttributeError: 'dict' object has no attribute 'packet'` does not come from the reassembly adapter at all; it fires in the PCAP trace-flow dumper at `pcapkit/dumpkit/pcap.py:139`, because dpkt and scapy hand the tracer plain dicts and `follow_tcp_stream` lets `trace_format` default to `'pcap'`. Handled here by upgrading to `'json'` for those engines -- silently when unset, with a `FormatWarning` when `'pcap'` was asked for explicitly. `extraction.py`'s guard is deliberately left alone: its own NOTE records that dpkt and scapy share the defect and were out of scope, so direct `Extractor` use still wants that guard extended separately. `engine='scapy'` returning 0 streams is not a bug: scapy does not recognise `in.pcap`'s link-layer type and yields all six frames as opaque `Raw`, so no frame carries TCP. The test asserts that *cause*, so a future scapy that learns the link type fails loudly rather than silently returning 0. The old tests mocked `engine` as a string, which is precisely why they never caught this, and that mocking cannot survive type-based dispatch -- so they are rewritten against real extraction of the committed `in.pcap`, with per-engine expected counts and the dpkt/scapy cases gated on the optional extras. Verified: dpkt goes from crashing to 3 streams, byte-identical to `default` including per-stream packet counts. tests/interface 12 passed; fixture-free unit tier 577 passed, 17 skipped against a 572/17 baseline -- the delta is exactly the new tests.
There was a problem hiding this comment.
🔵 Needs a closer look
Public type hints for format/Formats are now observably inconsistent with supported runtime values (e.g., 'cap' alias), and should be corrected to avoid misleading API typing for callers.
Pull request overview
Fixes follow_tcp_stream engine dispatch by selecting the correct TCP reassembly adapter based on the running engine instance type (instead of comparing the engine object to a string), and avoids the dpkt/scapy “dict frame + PCAP trace dumper” crash by switching trace output to a dict-capable format.
Changes:
- Dispatch TCP reassembly adapter via
isinstance(extraction.engine, ...)and aligncount=passing with adapter expectations. - Work around dpkt/scapy traceflow dumping crash by upgrading/downgrading
trace_formattojson(warn only when the user explicitly requested an unusable PCAP format). - Replace the previous mocked-engine tests with capture-based, per-engine regression tests (skipping when optional extras are not installed).
File summaries
| File | Description |
|---|---|
| pcapkit/interface/misc.py | Fixes engine adapter selection and adds dpkt/scapy trace-format workaround to prevent dict→PCAP dumper crashes. |
| tests/interface/test_misc.py | Rewrites tests to validate real per-engine behavior against the committed in.pcap capture, including optional-engine gating. |
Review details
Suppressed comments (1)
pcapkit/interface/misc.py:43
- The
FormatsLiteral excludes'cap', but bothExtractorandfollow_tcp_streamexplicitly treat'cap'as a PCAP alias (trace_format in ('pcap', 'cap', None)). This makes the public type hints inconsistent with supported runtime values and can lead to false-positive type errors for callers.
Formats = Literal['pcap', 'json', 'tree', 'plist']
# NOTE: this alias duplicates the one in ``pcapkit.foundation.extraction``;
# both copies need updating when a new engine lands.
Engines = Literal['default', 'pcapkit', 'dpkt', 'scapy', 'pyshark', 'pypcap', 'pypcapfile']
- Files reviewed: 2/2 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Matches the majority form already used across the suite, and the one GitHub auto-links.
Copilot's review on #402 was right, and the gap is pre-existing rather than introduced -- this change merely made it observable, since the dpkt/scapy trace workaround has to compare against 'cap'. `Formats` declared four values, while `Extractor.__output__` and `TraceFlowBase.__output__` both register eight: cap, json, pcap, plist, text, tree, txt, xml. So 'cap' and the 'txt'/'xml'/'text' aliases were accepted at runtime but unspellable for a type checker, and a caller passing one got a spurious error. Both declarations now match the registries exactly, verified against the live `__output__` keys rather than by eye. Two of those keys are defective for output -- 'pcap'/'cap' raise TypeError and 'text' raises AttributeError from dictdumper -- but they are registered, so they are part of the declared surface; the type should describe what the API accepts, not which paths happen to work. Both are pre-existing and unfiled.
|
Copilot's finding is correct, and the gap is pre-existing rather than introduced — this PR merely made it observable, because the dpkt/scapy trace workaround has to compare
Formats = Literal['pcap', 'json', 'tree', 'plist']while both registries accept eight, checked against the live objects rather than by reading the source: So One deliberate call worth recording: two of those eight keys are defective for output —
|
Closes #399 — with a correction to that issue's diagnosis, which was mine and was wrong about half of it.
The dispatch bug
follow_tcp_streamchose its reassembly adapter withextraction.engine == 'dpkt', butExtractor.enginereturns theEngineinstance, so both branches were dead code andpcapkit.toolkit.pcap's adapter was always used — against frames from whichever engine actually ran.The two
# type: ignore[comparison-overlap]comments were mypy reporting precisely this and being silenced. Both are gone and mypy is clean on the file without them. Note it was only "clean" before because the always-false comparison made thecount=call statically unreachable.Dispatch is now on the engine type, chosen over
.namedeliberately: for a third-party engine registered asclass Foo(Engine, name='foo')the registry key ('foo') and the.nameproperty ('Foo') diverge, so a name comparison is unreliable — and a third-party engine that subclasses a built-in now routes to the right adapter. An engine matching none of the built-ins warns and returns()rather than silently using the wrong one, which is the failure mode this issue is about.fallbackbecamepass_count, set per adapter rather than per branch. The pcapkit-native adapters read the frame number off the dissected frame and take nocount; the third-party ones have no intrinsic number and must be passedcount=index. The old code had these backwards.The second defect, and my mistake in the issue
I filed #399 claiming the dpkt
AttributeError: 'dict' object has no attribute 'packet'came from the reassembly adapter. It does not. Traced onmain:It fires during
Extractorconstruction, in the PCAP trace-flow dumper: dpkt and scapy hand the tracer plaindictframes, andfollow_tcp_streamletstrace_formatdefault to'pcap', which that dumper cannot re-serialise.This is a known, deliberately deferred defect —
pcapkit/foundation/extraction.py:836-846says so in aNOTE, and the guard on line 847 covers only'pyshark'and'pypcapfile':Handled here inside
interface/misc.py— upgrading to'json'for dpkt and scapy, silently when unset and with aFormatWarningwhen'pcap'was requested explicitly — rather than editing sharedextraction.py, to respect that recorded deferral. Still open afterwards: directExtractoruse is untouched by this workaround, so extending theextraction.py:847guard remains worth doing separately.scapy returning 0 streams is not a bug
Also a correction to the issue. scapy does not recognise
in.pcap's link-layer type (unknown LL type [1]) and yields all six frames as opaqueRaw, so no frame carries TCP and there is genuinely no stream to follow. The test asserts that cause, so a future scapy that learns the link type fails loudly instead of silently returning 0.Verification
defaultdpktscapyThe old tests mocked
engineas a string — which is exactly why they never caught this — and that mocking cannot survive type-based dispatch, so they are rewritten against real extraction of the committedin.pcap. Per-engine expected counts rather than a uniform number, since engines with genuine capability gaps legitimately differ. dpkt/scapy cases are gated on the optional extras so a fresh[test]clone skips rather than fails.tests/interface: 12 passed. Fixture-free unit tier: 577 passed, 17 skipped against a 572/17 baseline measured on the unmodified worktree — the delta is exactly the new tests.Also checked
grep -rn "comparison-overlap"finds two more, atprotocols/internet/hopopt.py:1004andipv6_opts.py:1016. Those are a different, benign pattern —schema.seed is not NoValue, where the field is typedintbut defaults to the sentinel at runtime, so the guard is correct and the ignore hides an annotation gap rather than a dead comparison. Left untouched. No other object-vs-string comparisons exist inpcapkit/.