protocols: size MH's MN-ID option from its subtype, not identifier's Python type - #464
Conversation
…Python type _make_opt_mn_id sized the identifier by inspecting isinstance(identifier, ...) rather than the resolved subtype_val that actually selects the wire format (mn_id_selector). For the IPv6_Address subtype -- the method's own default -- the schema always packs a fixed 16-octet address regardless of declared length, so a str or int identifier, including the no-argument default, declared a length that disagreed with what was packed. - pcapkit/protocols/internet/mh.py: size id_len from subtype_val (16 for IPv6_Address) and normalise identifier to an ipaddress.IPv6Address first, so the packed bytes and the declared length derive from one value. - examples/generators/options.py: drop the MN_ID_OPTION_TYPE override that worked around the defect by forcing an already-correct-type identifier; the generator now exercises the real (now-fixed) default. - tests/protocols/internet/test_mh_unit.py: add a dedicated test asserting len(packed) == length + 2 for every documented identifier type against IPv6_Address, including the default; fix an existing assertion that had encoded the buggy length (3) as expected. - docs/source/pep.rst: drop the deferred-defect note now that it is fixed. Re-measured against faf86d2: PR #437 rewrote this module wholesale but did not fix this method, only worked around it in the generator. Full suite: 995 passed/17 skipped/1547 subtests before, 996 passed/17 skipped/1553 subtests after, no failures either side, baseline faf86d2. Closes #448
What I read
Reviewed at head The diff
Item 1 — is sizing from
|
| collected | passed | skipped | subtests passed | |
|---|---|---|---|---|
Before (faf86d26b, pristine) |
1012 | 995 | 17 | 1547 |
After (04e3cca92) |
1013 | 996 | 17 | 1553 |
Both exactly match the PR's own claimed numbers. "Before" took 852s, "after" 833s (wall clock varied
with contention from other concurrent worktree sessions on this host, as the task brief warned).
17 skipped both sides — RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') are all present
in this venv (HAS_RUNTIME true), so the 3 gated tests in test_option_coverage_runtime.py ran
rather than skipped; I did not investigate why the count is ever 20 elsewhere, since the brief
explicitly says that trigger is unexplained and not to invent a reason.
Verdict
GOOD TO MERGE, sha 04e3cca92 (and equally 5d869165f, its current head, by the empty-diff
pass-through-merge argument above).
The fix is correct and narrowly scoped to exactly what it claims: IPv6_Address sizing now derives
from subtype_val instead of Python type, verified across all four documented identifier types with
matching declared/packed lengths in every case. The test change, the override removal, the docs
removal, and the import hygiene all check out against independent measurement, not just the PR's own
narration. CI is fully green. The one thing I'd flag for a follow-up rather than as a blocker: the
elif isinstance(identifier, int) branch in the same method still has the type-vs-subtype confusion
issue #448 describes, for every subtype other than IPv6_Address — pre-existing, untouched by this
diff, unexercised by any test, and outside what #448/#464 scope themselves to, but real (demonstrated
above) and worth its own issue.
main gained #461 (closing #458: prepare's @prepare decorator now distinguishes a declared zero length from a derived one, raising StreamEOFError only for the latter) since this branch's previous merge, on top of #462 (closing #459, already handled). Between the two, all three remaining httpv2-frame entries this PR's own fix had exposed -- DATA, HEADERS, CONTINUATION -- now round-trip cleanly too. Verified directly against the round-trip harness: all three return 'OK'. Rewrote the HTTP/2 section's comment block to summarise all six frames' history (PUSH_PROMISE/PING via #445 itself, DATA/HEADERS/ CONTINUATION via #461, SETTINGS via #462) now that none of them need an entry. Also merged origin/main (fa12895, #461) -- clean auto-merge, no conflicts, confirmed against #464's changes to tests/protocols/internet/test_mh_unit.py (different hunks, and its own test run clean: 62 passed, 272 subtests, 0 failed). Verified: mypy pcapkit -> 124 errors/40 files (main at fa12895: 125, one more than its previous 124 -- unrelated to this branch, and this branch stays one fewer than whatever main's own count is, from the same pre-existing type: ignore cleanup as before). Round-trip harness: 7 passed, 363 subtests passed, 0 failed. mh-extension/* re-verified 'OK' again on this merge (all four).
Summary
MH._make_opt_mn_idsized the MN-ID option's identifier from the Pythontype of the
identifierargument, not from thesubtypethat actuallyselects the wire format (
mn_id_selector). For theIPv6_Addresssubtype —the method's own default — the schema always packs a fixed 16-octet address
(
IPv6AddressFieldignores any declared length entirely), so astrorintidentifier declared alengththat disagreed with what was packed.Re-measured on
faf86d26b(origin/main)PR #437 rewrote this module wholesale (all 24 message types, 70/71 options,
4 CGA extensions) but did not fix this method — it only worked around
the defect in the test-fixture generator (
examples/generators/options.py),whose own comment names the bug:
Directly re-measuring
_make_opt_mn_idagainstfaf86d26breproduces theissue's table exactly:
identifierargumentlengthlength + 2'::'— the method's own default'2001:db8::1'(str)ipaddress.ip_address('2001:db8::1')After the fix, all three agree (
length == 17,packed == 19,length + 2 == 19).Fix
pcapkit/protocols/internet/mh.py: sizeid_lenfrom the resolvedsubtype_valrather than fromisinstance(identifier, ...). ForEnum_MNIDSubtype.IPv6_Address,id_lenis now always16, andidentifieris normalised to anipaddress.IPv6Addressfirst (mirroringthe existing
_make_opt_lmaapattern in the same file), so the packedbytes and the declared length derive from one value instead of two
independent computations. Other subtypes (
NAI, and theBytesFieldfallback) were already sized correctly and are unchanged.
examples/generators/options.py: removed theEnum_Option.MN_ID_OPTION_TYPEoverride that worked around the defect byforcing an already-correct-type identifier (
ipaddress.IPv6Address('::1'))— the generator now exercises the real, now-fixed default (
'::').docs/source/pep.rst: removed the "filed rather than fixed" bulletfor MH MN-ID option sizes its identifier from the Python type rather than the subtype, so even the default arguments mis-declare length #448 under the Mobility Header section, now that it's fixed.
Siblings checked, none found
Only four
_make_opt_*methods inmh.pytake asubtypeparameter:_make_opt_mn_id,_make_opt_auth,_make_opt_mn_group,_make_opt_mag_id.Checked each:
_make_opt_authsizes fromlen(data)wheredatais strictly typedbytes— no type union, no confusion._make_opt_mn_groupuses a fixedlength=6regardless of subtype._make_opt_mag_idtypesidentifierstrictly asbytes(no union), andits schema (
MAGIdentifierOption.identifier) is an unconditionalBytesField, not a subtype-dispatchedSwitchFieldlikeMNIDOption—so there's no type-vs-subtype mismatch possible there.
Grepped the whole file for
isinstance(..., (IPv6Address|int))-based sizing;the only other occurrences are unrelated (
_seconds/lifetime helpers,_make_opt_lmaa's address-family branch, which already normalises-then-sizescorrectly and was the style precedent for this fix). No sibling defect found.
Tests
tests/protocols/internet/test_mh_unit.py:test_mh_mn_id_option_length_matches_packed_octets— assertslen(packed) == length + 2for every documentedidentifiertype(
str,int,bytes,IPv6Address) against theIPv6_Addresssubtype,including the method's own no-argument default.
test_mh_option_constructors_cover_known_options_and_dispatchhad
proto._make_opt_mn_id(Option.MN_ID_OPTION_TYPE, identifier=0x1234).lengthasserted equal to
3— that was the buggy value under the defaultIPv6_Addresssubtype. Now asserts17.Before the fix (pristine
faf86d26b, with these test edits applied): 4subtests of the new test failed (
3 != 17/12 != 17), plus the fixedassertion failed on its own —
5 failed, 35 passed, 268 subtests passedfor
test_mh_unit.pyalone. After the fix:36 passed, 0 failed, 272 subtests passed.tests/protocols/test_option_roundtrip_unit.py: noEXPECTED_FAILURESentry existed for
mh-option/MN_ID_OPTION_TYPEbefore or after — it waspassing both times (363 subtests, 7 tests, unchanged), because the harness
was exercising the override's already-correct-type identifier rather than
the real default. Nothing in
EXPECTED_FAILURESflipped. No new_mh_option_overridesentry was needed; an existing one (a workaround forthis exact defect) was removed instead — see above.
Full suite (baseline
faf86d26b)Run with
PYTHONSAFEPATH=1, fresh fixtures viaexamples/generators/make_samples.pyeach time, same methodology bothsides (
--collect-onlycounts, then full run):faf86d26b)No failures on either side — the pristine suite never caught this defect;
the delta is exactly the one new test method (6 new subtests) plus the
corrected assertion in the existing one.
Test plan
faf86d26b(table above)_make_opt_*methods for the same confusion — none foundCloses #448