Skip to content

docs(changelog): shared 1.5.0 changelog — long-lived, merges last (#610, #616, #617, #618, #620) - #657

Open
JarryShaw wants to merge 30 commits into
mainfrom
docs/changelog-1.5.0
Open

JarryShaw wants to merge 30 commits into
mainfrom
docs/changelog-1.5.0

Conversation

@JarryShaw

Copy link
Copy Markdown
Owner

This PR is deliberately long-lived. Please do not merge it yet.

It is the shared changelog for the 1.5.0 cycle. Code pull requests in this cycle
ship without their own changelog entry, and their entry is committed here instead.
It stays open and keeps accumulating commits as more code PRs land, and it merges
last, once the code PRs are settled.

If you have arrived here wondering whether it is stalled: it is not. An open,
growing diff is the intended steady state.

Why it exists

Five open PRs each added one bullet to docs/source/changelog/1.5.0.rst at the same
anchor and regenerated CHANGELOG.md beside it. That made them mutually exclusive
rather than independent — whichever merged first moved the anchor, and the other four
immediately re-conflicted on the two changelog files, even though no code file of
theirs overlapped at all
. That cycle had already cost this wave thirteen rebases.

Separating the changelog from the code breaks the cycle: the code PRs stop contending
for a shared file, and the contention is concentrated in one place — here — where it
is a sequence of appends rather than a conflict.

What it currently covers

One commit per code PR, so it is obvious what is and is not accounted for:

Entry Issue Code PR
TCP.read seeding its flag accumulator with a typing.cast no-op #616 #634
Frame.len / cap_len swapped between the PCAP and PCAP-NG readers #618 #635
Extractor closing the caller's stream and leaking its own #610 #636
extract(..., no_eof=True) never returning #620 #639
a construction keyword no signature declares now refused #617 #640

Every bullet is the verbatim block its own PR added — not reworded, not reflowed,
not trimmed, and not reordered internally. The entry-file diff is a single hunk of 197
added lines with zero deletions, which is the mechanical guarantee of that.

Two deliberate choices

It is not squashed, and should not be. The one-commit-per-PR convention does not
fit a PR that accumulates over days. A commit per code PR keeps it reviewable and
makes coverage self-evident. Please do not "tidy" it into a single commit.

The first commit is a fix, not an entry. 2c4212a02 regenerates CHANGELOG.md,
which has been drifted since 375e9d411 (#638) hand-inserted three lines into the
generated file instead of running util/changelog_md.py. That left a stale copy of
the #630 bullet sitting below its own correction, and stranded the #631 bullet after
#638.

That drift is why main is currently red, and it is unrelated to any of the five:
python util/changelog_md.py --check exits 1 on main, failing the Changelog drift
job and all twelve matrix jobs, the latter through the same assertion in
tests/project/test_changelog_md.py::RepositoryStateTests. Measured on run
35748204467 (a62aed134): 13 of 14 jobs failed.

It is kept as the first commit specifically so it can be cherry-picked ahead of the
rest of this branch
if you want main green before this PR merges:

git cherry-pick 2c4212a02

python util/changelog_md.py --check exits 0 at every one of the six commits here.

Still to come on this branch

  • An editorial pass restructuring the 1.5.0 entry — an executive summary up front,
    detail below — which is tracked separately and lands here as its own commit.
  • Further entries as more code PRs land.

The bullet #634 originally carried, moved here verbatim so that #634 touches only
`pcapkit/protocols/transport/tcp.py` and its two test files.

Covers: `TCP.read` seeding its connection-flag accumulator with a `typing.cast`
no-op rather than `Flags(0)`, so a flagless segment left `self._flags` a plain
`int`.

35 lines added to the entry file; `CHANGELOG.md` regenerated, not edited.
The bullet #635 originally carried, moved here verbatim so that #635 touches only
the two `Frame` modules, `pcapkit/toolkit/pcapng.py` and its two test files.

Covers: the breaking change to a public attribute -- `Frame.len` is the on-wire
length and `cap_len` the captured one, which the PCAP and PCAP-NG readers had
filled from opposite wire fields.

41 lines added to the entry file; `CHANGELOG.md` regenerated, not edited.
The bullet #636 originally carried, moved here verbatim so that #636 touches only
`pcapkit/foundation/extraction.py` and its two test files.

Covers: `Extractor` closing the caller's input stream and leaking the one it
opened itself, both handlers now reading a single `_owns_input` predicate.

23 lines added to the entry file; `CHANGELOG.md` regenerated, not edited.
The bullet #639 originally carried, moved here verbatim so that #639 touches only
`pcapkit/foundation/extraction.py`, `pcapkit/interface/core.py` and its three test
files.

Covers: `extract(..., no_eof=True)` never returning, and the progress check that
now ends it -- including the deliberate narrowing for a seekable input still being
appended to.

43 lines added to the entry file; `CHANGELOG.md` regenerated, not edited.
The bullet #640 originally carried, moved here verbatim so that #640 touches only
`pcapkit/protocols/protocol.py`, `pcapkit/protocols/application/http.py`,
`docs/source/ext.rst`, `examples/generators/dispatch.py` and its six test files.

Covers: the behaviour change to a public API -- building a protocol through its
constructor with a keyword no signature declares now raises `UnsupportedCall`
instead of discarding it.

55 lines added to the entry file; `CHANGELOG.md` regenerated, not edited.
JarryShaw added a commit that referenced this pull request Sep 22, 2026
…t values (#653, #654, #655)

`_make_param_puzzle` and `_make_param_solution` derived three wire-format
quantities from the payload value rather than taking them from the data model.
All three derivations were wrong, in the same two functions, and they are fixed
together because the width resolution is one expression that cannot be written
twice.

* The field width came from `int.bit_length()` and nothing else, so every
  leading zero octet was dropped on re-serialisation. A SOLUTION read with
  `Length = 20` rebuilt as `Length = 6`, a PUZZLE read with `Length = 12` as
  `Length = 5` -- silently, since the integers survive and nothing raises. Both
  data models now carry `rhash_len`, the field's on-wire width in bits, and both
  builders prefer it. This only became reachable end to end once #608 was fixed
  (#629); before that the undersized rebuild tripped the reader's parity guard
  first and failed loudly. (#653)
* SOLUTION's second contents octet is `Reserved`, "zero when sent, ignored when
  received" (RFC 7401 5.2.5, RFC 5201 5.2.5 identically) -- not a PUZZLE
  `Lifetime`, which only RFC 7401 5.2.4 defines. pcapkit wrote `0x20`, `0x21`,
  `0x25` or `0x2b` there and could not write the mandated zero at all: a
  conformant SOLUTION with `Reserved = 0x00` parsed to `timedelta(0)` and then
  escaped a bare `ValueError` from `math.log2(0.0)`. `Data_SolutionParameter`
  and `SolutionParameter` now carry `reserved` verbatim, so the conformant zero
  round-trips and a received non-zero octet is reproduced rather than
  re-derived. (#654)
* Neither builder read its own `version` keyword, so `version=1` and
  `version=2` computed identical lengths at every bit width. RFC 5201 5.2.4 and
  5.2.5 state both fields as literally 8 bytes and `Length` as literally 12 and
  20, and the readers enforce exactly that -- so under HIPv1 each builder
  accepted only a `bit_length()` of 57..64 and built, for everything else, a
  parameter this library's own reader rejects. Width now comes from the version
  under HIPv1. (#655)

The remaining `math.log2` sites, both in PUZZLE where a `Lifetime` really
lives, now raise `ProtocolError` rather than letting `ValueError` escape.
`ProtocolError(BaseError, ValueError)` is what the readers already raise for a
malformed PUZZLE or SOLUTION, and deriving from the builtin it replaces keeps
any caller written around today's bare `ValueError` working; `EnumError` is
`(BaseError, TypeError)` and would silently stop being caught. The same guard
covers the upper end, because `UInt8Field` wraps rather than raising --
measured, `300` packs as `0x2c` -- so an out-of-range lifetime would otherwise
be written as some other valid-looking duration. A plain `float` lifetime used
to escape an `AttributeError` from the `isinstance(lifetime, int)` branch; the
test keyed on `timedelta` instead found that.

`_make_param_solution` no longer accepts `lifetime=`. `reserved=` and
`rhash_len=` are both `None`-sentinelled, so an explicit value overrides what a
parsed parameter carries: `Data_SolutionParameter` is immutable, so without that
a caller holding a parsed parameter had no way to write the conformant zero over
a peer's non-conformant `Reserved`. The plain data fields still let `param` win,
as they did before. `rhash_len=` is also the only way a from-scratch HIPv2 build
can state a width, which genuinely varies with the Responder's HIT Suite
(RFC 7401 2.3, 5.2.10).

Five new tests, each verified to fail on 0c7f2b7 for the defect's own reason
and pass here: `12 != 5` and friends for #655, `expected a positive input` and
`'reserved' not found` for #654, `no attribute 'rhash_len'` for #653. The
widths exercised are 1, 9, 15, 17, 57 and 65 bits, where the candidate formulas
disagree, with the byte-aligned widths kept as controls -- at a multiple of 8
the correct `2 * ceil(b / 8)` coincides with #608's `ceil(b / 4)`, which is why
every byte-aligned fixture passed through that defect unharmed.

Two cases are deliberately accepted rather than rejected, and now say so in
`_make_puzzle_field_width`'s docstring: `rhash_len == 0`, which is what the
derived path yields for the default `random=0` and what a `Length = 4`
parameter parses back to; and a `version` other than 1, which is treated as
HIPv2 exactly as both readers' `version == 1` guards do.

Coverage holds at 100% statement and branch on all three changed modules, with
statements 1385 -> 1414 and branches 316 -> 338; 30 tests / 434 subtests ->
35 / 455. Scoped unit tier green: 291 passed, 1191 subtests. No
EXPECTED_FAILURES entry moved -- still 45, with the same four HIP entries.

No changelog entry: that is consolidated in #657.
JarryShaw added a commit that referenced this pull request Sep 22, 2026
…t values (#653, #654, #655)

`_make_param_puzzle` and `_make_param_solution` derived three wire-format
quantities from the payload value rather than taking them from the data model.
All three derivations were wrong, in the same two functions, and they are fixed
together because the width resolution is one expression that cannot be written
twice.

* The field width came from `int.bit_length()` and nothing else, so every
  leading zero octet was dropped on re-serialisation. A SOLUTION read with
  `Length = 20` rebuilt as `Length = 6`, a PUZZLE read with `Length = 12` as
  `Length = 5` -- silently, since the integers survive and nothing raises. Both
  data models now carry `rhash_len`, the field's on-wire width in bits, and both
  builders prefer it. This only became reachable end to end once #608 was fixed
  (#629); before that the undersized rebuild tripped the reader's parity guard
  first and failed loudly. (#653)
* SOLUTION's second contents octet is `Reserved`, "zero when sent, ignored when
  received" (RFC 7401 5.2.5, RFC 5201 5.2.5 identically) -- not a PUZZLE
  `Lifetime`, which only RFC 7401 5.2.4 defines. pcapkit wrote `0x20`, `0x21`,
  `0x25` or `0x2b` there and could not write the mandated zero at all: a
  conformant SOLUTION with `Reserved = 0x00` parsed to `timedelta(0)` and then
  escaped a bare `ValueError` from `math.log2(0.0)`. `Data_SolutionParameter`
  and `SolutionParameter` now carry `reserved` verbatim, so the conformant zero
  round-trips and a received non-zero octet is reproduced rather than
  re-derived. (#654)
* Neither builder read its own `version` keyword, so `version=1` and
  `version=2` computed identical lengths at every bit width. RFC 5201 5.2.4 and
  5.2.5 state both fields as literally 8 bytes and `Length` as literally 12 and
  20, and the readers enforce exactly that -- so under HIPv1 each builder
  accepted only a `bit_length()` of 57..64 and built, for everything else, a
  parameter this library's own reader rejects. Width now comes from the version
  under HIPv1. (#655)

The remaining `math.log2` sites, both in PUZZLE where a `Lifetime` really
lives, now raise `ProtocolError` rather than letting `ValueError` escape.
`ProtocolError(BaseError, ValueError)` is what the readers already raise for a
malformed PUZZLE or SOLUTION, and deriving from the builtin it replaces keeps
any caller written around today's bare `ValueError` working; `EnumError` is
`(BaseError, TypeError)` and would silently stop being caught. The same guard
covers the upper end, because `UInt8Field` wraps rather than raising --
measured, `300` packs as `0x2c` -- so an out-of-range lifetime would otherwise
be written as some other valid-looking duration. A plain `float` lifetime used
to escape an `AttributeError` from the `isinstance(lifetime, int)` branch; the
test keyed on `timedelta` instead found that.

`_make_param_solution` no longer accepts `lifetime=`. `reserved=` and
`rhash_len=` are both `None`-sentinelled, so an explicit value overrides what a
parsed parameter carries: `Data_SolutionParameter` is immutable, so without that
a caller holding a parsed parameter had no way to write the conformant zero over
a peer's non-conformant `Reserved`. The plain data fields still let `param` win,
as they did before. `rhash_len=` is also the only way a from-scratch HIPv2 build
can state a width, which genuinely varies with the Responder's HIT Suite
(RFC 7401 2.3, 5.2.10).

Five new tests, each verified to fail on 0c7f2b7 for the defect's own reason
and pass here: `12 != 5` and friends for #655, `expected a positive input` and
`'reserved' not found` for #654, `no attribute 'rhash_len'` for #653. The
widths exercised are 1, 9, 15, 17, 57 and 65 bits, where the candidate formulas
disagree, with the byte-aligned widths kept as controls -- at a multiple of 8
the correct `2 * ceil(b / 8)` coincides with #608's `ceil(b / 4)`, which is why
every byte-aligned fixture passed through that defect unharmed.

The byte-exact assertions compare the parameter without its trailing padding,
and then against the re-packed source schema rather than against a literal, so
they pin the `Length` field and the payload octets without encoding a padding
rule that #651/#664 is concurrently changing. Verified against a `git
merge-tree` of this branch and #664: both library files auto-merge with no
conflict, and all five new tests pass against the merged library.

Two cases are deliberately accepted rather than rejected, and now say so in
`_make_puzzle_field_width`'s docstring: `rhash_len == 0`, which is what the
derived path yields for the default `random=0` and what a `Length = 4`
parameter parses back to; and a `version` other than 1, which is treated as
HIPv2 exactly as both readers' `version == 1` guards do.

Coverage holds at 100% statement and branch on all three changed modules, with
statements 1385 -> 1414 and branches 316 -> 338; 30 tests / 434 subtests ->
35 / 455. Scoped unit tier green: 291 passed, 1191 subtests. No
EXPECTED_FAILURES entry moved -- still 45, with the same four HIP entries.

No changelog entry on this branch: that is consolidated in #657.
…ships in #665

The bullet #665 would otherwise have carried, kept here so that #665 touches only
`pcapkit/protocols/internet/hip.py`, `pcapkit/protocols/schema/internet/hip.py`,
`pcapkit/protocols/data/internet/hip.py` and `tests/protocols/internet/test_hip_unit.py`.

Covers all three as one bullet, because they are one root cause: the HIP `PUZZLE`
and `SOLUTION` builders derived the field width, the `Reserved` octet and the
version-dependent length from the payload value instead of from the data model.
Splitting the entry would tell the story three times and explain it none.

Two public data models change, so the bullet says so in bold and carries a
migration sentence: `SolutionParameter.lifetime` becomes `reserved` and an `int`
rather than a `timedelta`, both parameter models gain a required `rhash_len`, and
`_make_param_solution` no longer takes `lifetime=`.

46 lines added to the entry file; `CHANGELOG.md` regenerated with
`util/changelog_md.py`, not edited. `--check` exits 0 and `tests/project/` is green
at 96 passed, 469 subtests.

Committed from a detached HEAD on 69a6e13 and pushed to the branch ref, because
`docs/changelog-1.5.0` is checked out in another agent's worktree at a stale
f846523 and could not be taken here.
The bullet #664 would otherwise have carried, kept here so that #664 touches only
`pcapkit/protocols/internet/hip.py`, `pcapkit/protocols/schema/internet/hip.py`,
`tests/protocols/internet/test_hip_unit.py`,
`tests/protocols/test_option_roundtrip_unit.py`,
`examples/generators/options.py` and `docs/source/pcapkit/protocols/internet/hip.rst`.

One bullet, because it is one root cause in 95 places: every HIP padding site
aligned the parameter's *contents* to eight octets rather than the record,
ignoring the four-octet type-and-length header, so every parameter pcapkit wrote
was `4 (mod 8)` for every possible `Length`.

The bullet says in bold that both the emitted octets and the data model's
reported `length` change, and carries a migration sentence: a `SEQ` parameter's
`length` is 8 where it was 12, so code comparing stored output byte for byte or
asserting on `Data_*Parameter.length` sees different values.

It also records what was deliberately *not* changed, since both look like part of
the same defect and are not: `HIP.make`'s `len = total_length // 8 + 4`, which
RFC 7401 section 5.1.3 shows is correct and merely needed 8-aligned parameters;
and `HIP_COPIES`, which stays at two for `R1_COUNTER`'s four-octet `counter`
against section 5.2.3's eight -- a separate, still-unfiled defect this one had
been masking. The `EncryptedParameter.data` length callback is named as fixed in
the same change because the two four-octet errors cancelled at four of the eight
residues of `Length`, so correcting the padding alone would have regressed it.

41 lines added to the entry file; `CHANGELOG.md` regenerated with
`util/changelog_md.py`, not edited. `--check` exits 0 and `tests/project/` is
green at 96 passed, 469 subtests.

Committed from a detached HEAD on 2ce3687 and pushed to the branch ref, because
`docs/changelog-1.5.0` is checked out in another agent's worktree at a stale
f846523 and could not be taken here. Note 2ce3687, not the 69a6e13 I was
given: the branch had already moved on with #665's entry.
The bullet #667 would otherwise have carried, kept here so that #667 touches only
`pcapkit/corekit/multidict.py` and `tests/corekit/test_multidict.py`.

One bullet, because it is one convention gap in one class: `_Missing` behind
`MultiDict.pop` and `OrderedMultiDict.pop` lacked the `@final` and the falsy
`__bool__` that `NoValueType` in `pcapkit.corekit.fields.field` sets as the
package's convention for a marker of this kind.

The bullet says plainly that no behaviour changes, and says why rather than
asserting it: both `pop()` implementations decide by identity, never by
truthiness, and `pop()` structurally cannot return the marker -- it returns
`default` only on the branch where `default is not _missing`. It also names the
one way the old truthiness was observable, which is what justifies touching it at
all: `inspect.signature(MultiDict.pop).parameters['default'].default` hands the
marker to any caller who asks, and `if default:` on it reported "a default was
supplied" where none had been.

It closes by recording the disposition of the other two sites from the #640
sweep, so the entry is the whole story: site 1 needed nothing, and `_NOT_FOUND`
in `pcapkit.utilities.compat` stays a bare `object()` deliberately, being a
verbatim line of CPython's `functools.cached_property` inside a
`sys.version_info < (3, 8)` branch no supported interpreter reaches. The
reasoning behind that one is on #661, not here.

`:obj:` roles had to come out: `util/changelog_md.py` rejects them with
`ResidualMarkupError`, since its six conversion rules do not cover interpreted
text and `CHANGELOG.md` would carry the role through as literal text. Double
backticks instead, which is what the rest of the entry file uses.

20 lines added to the entry file; `CHANGELOG.md` regenerated with
`util/changelog_md.py`, not edited. `--check` exits 0 and `tests/project/` is
green at 96 passed, 469 subtests.

Committed from a detached HEAD on e55ba36 and pushed to the branch ref, because
`docs/changelog-1.5.0` is checked out in another agent's worktree at a stale
f846523 and could not be taken here. Note e55ba36, not the 69a6e13 I was
given: the branch had already moved on with #665's and #651's entries.

Refs #661
#669

One bullet, because it is one round trip with a defect on each side of it, in the
same two files: `_make_http_data` never read `frame.flags` on the construct side,
and `FrameType.post_process` seeded its accumulator with a bare `0` on the parse
side.

The bullet leads with what changes rather than with the mechanism, since both
halves alter output: the reconstructed DATA frame's flags octet, and the dumped
`__value__` of a flagless frame. It says why #650 was worth fixing at all, which
its issue had left as an open question -- the dump rendered `__value__` as a JSON
number for a flagless frame and a JSON string for every other frame in the same
capture, so the fix removes a type inconsistency rather than introducing one.

It also records two things a reader would otherwise be surprised by. The seed is
guarded rather than unconditional, because `FrameType.Flags` has no members and a
memberless `enum.Flag` subclass refuses `Flags(0)` -- the one-token fix the issue
proposed would have crashed six of the twelve frame schemas. And a DATA round trip
is still lossy after this, for the unrelated mis-parenthesised length callbacks
filed as #668, so the entry does not let the reader infer a clean round trip that
does not exist yet.

The `TypeError` message had to sit on one line: `util/changelog_md.py` rejects a
`` literal spanning a line break with `ResidualMarkupError`, since its six
conversion rules do not cover it.

37 lines added to the entry file; `CHANGELOG.md` regenerated with
`util/changelog_md.py`, not edited. `--check` exits 0 and `tests/project/` is green
at 96 passed, 469 subtests.

Committed from a detached HEAD on 367b6e6 and pushed to the branch ref, because
`docs/changelog-1.5.0` is checked out in another agent's worktree and could not be
taken here. Note 367b6e6, not the 69a6e13 I was given: the branch had already
moved on with #665's, #651's and #661's entries.

Refs #652
Refs #650
…s in #670 and #671

Two bullets, not one, because the two defects are unrelated: one changes what the
dumpers emit, the other only the text of four exception messages. They share a
file only by accident of being found in the same pass.

The #648 bullet leads with the output change and says so in bold, because that is
what a reader upgrading needs to see: a flag value with no declared bits dumped
as `Type::None [0]` in all six textual format names, out of both `Extractor` and
`TraceFlow`. It then justifies the *replacement* rather than just stating it,
since "render it as its decimal value" looks arbitrary until you know the
enumeration libraries already spell an undeclared residue that way -- and that a
decimal cannot collide with a member name where `None` can, `NONE` being a real
declared name elsewhere. Three things a reader would otherwise get wrong are
recorded: three sites carried the interpolation and not one, the guard is on
`name is None` rather than on zero because the defect never was about zero, and
it is not an `aenum` quirk since stdlib `enum.IntFlag` behaves identically.

It also corrects the issue on a point of fact. #648 said `Flags` was the only
registry nameless at zero; a sweep of all seven finds five, the four Mobility
Header flag registries included. And it states that the committed example dumps
do not move, which was measured by regenerating all three with and without the
change rather than assumed -- a reader of a bullet this emphatic will otherwise
wonder whether `examples/captures/` drifted.

The #649 bullet says "cosmetic" in its second sentence so nobody reads it as a
behavioural change, then gives the one reason it was worth doing at all: it is
the text a user sees when an option is rejected. It names all four sites, and the
28-against-4 count in the same file, because that count is what makes the correct
form a fact about the module rather than a preference.

Neither bullet claims a guard it does not have: #648's third site, the `addon`
branch, is not reachable from any registry in the library today, and the bullet
does not imply otherwise.

`CHANGELOG.md` regenerated with `util/changelog_md.py`, not edited by hand.
`--check` exits 0 and `tests/project/` is green at 96 passed, 469 subtests. Each
`` literal is kept on one line, since the generator rejects one spanning a line
break with `ResidualMarkupError`.

Committed from a detached HEAD on d14577d and pushed to the branch ref, because
`docs/changelog-1.5.0` is checked out in another agent's worktree and could not be
taken here. Note d14577d, not the 69a6e13 I was given: the branch had already
moved on with #665's, #651's, #661's and #652/#650's entries.

Refs #648
Refs #649
…entry

The bullet added in d14577d said a memberless `enum.Flag` subclass "refuses
`Flags(0)` outright", flatly. That is only true from Python 3.12, where the enum
rewrite made `EnumType.__call__` raise for an enum with no members; earlier
interpreters take the plain value-lookup path and hand back a pseudo-member.
Measured on the two available here:

    version 3.14.7  members: 0  Flags(0) -> TypeError: <flag 'Flags'> has no members
    version 3.7.16  members: 0  Flags(0) -> OK <Flags.0: 0>

`requires-python` is `>=3.6`, so the unqualified form overstated it. Three words
added, no other change to the bullet: the guard in #669 is correct on every
supported interpreter either way, being keyed on the memberless-ness rather than
on the refusal.

The same imprecision was corrected in #669's own source comment and PR body, and
its test now gates only the `TypeError` assertion behind
`sys.version_info >= (3, 12)` -- CI runs the unit tier on 3.10 through 3.15, so an
unconditional `assertRaises` would have gone red on the older two.

`CHANGELOG.md` regenerated with `util/changelog_md.py`, not edited; `--check` exits
0 and `tests/project/` is green at 96 passed, 469 subtests.

Committed from a detached HEAD on 6a956c4 and pushed to the branch ref, because
`docs/changelog-1.5.0` is checked out in another agent's worktree. The branch had
moved on with #648's and #649's entries since d14577d.

Refs #652
Refs #650
JarryShaw added a commit that referenced this pull request Sep 22, 2026
…a false packaging claim (#642)

Three names appeared in string annotations that their module never imported, and
they were mypy's complete set of ``name-defined`` findings for the package:

* ``pcapkit/utilities/logging.py:350`` used ``Any``; added to the
  ``TYPE_CHECKING`` block beside ``IO``, ``Optional`` and ``Union``.
* ``pcapkit/protocols/schema/internet/ipv6_route.py:136`` used ``Protocol`` and
  ``:271`` used ``Optional``; added, ``Protocol`` as
  ``ProtocolBase as Protocol``. Twenty-one sibling schema modules already spell
  it that way, in the same ``payload:`` stub; this makes twenty-two.

mypy 2.3.1 over ``pcapkit``: 3 ``name-defined`` errors before, 0 after; 115 total
errors before, 112 after, so nothing else moved.

``MANIFEST.in:14-17`` asserted that ``include README.md`` was "the only thing
that puts it in an sdist" and that an sdist without it "cannot be installed at
all". Both halves are false, and the file had contradicted itself since #631
wrote the correct mechanism seven lines below without correcting this. Deleting
the lines and rebuilding gives a byte-identical sdist listing -- empty diff --
that installs with exit 0: ``setuptools/command/sdist.py:59-60`` ships the README
unconditionally and ``setuptools/dist.py:460``'s default ``license_files`` glob
ships ``LICENSE``. Of the original three ``include`` lines only ``CHANGELOG.md``
is load-bearing. The comment now says that.

New ``tests/project/test_annotation_names.py`` resolves every string annotation
in the package -- following a nested forward reference such as
``'list["Nested"]'``, while treating ``Literal`` members and ``Annotated``
metadata as the values they are -- against the names its own module binds. It
reports the same three findings as mypy on the unfixed tree and none after.

That module named ``ast.TypeAlias`` and ``ast.TypeVar`` directly, and both are
PEP 695 nodes added in Python 3.12, so *every* test in it raised
``AttributeError`` on the 3.10 and 3.11 matrix jobs -- ``bound_names`` walks every
node of every file, so the attribute is reached whatever a test does. Both are now
resolved once at module scope through ``getattr(ast, ..., ())``, leaving the
``isinstance`` branches otherwise untouched: ``isinstance(x, ())`` is always
False, so the branches stay live on 3.12+ and are simply unreachable below it.
Chosen over a ``sys.version_info`` comparison because it writes no version number
down at all -- a comparison states 3.12 next to the attribute it guards, and the
two can then drift -- and over a per-node ``getattr`` because a module-level
constant lifts the lookup out of a loop that runs on every node of every file.

``ast.TypeVar`` is the branch that earns its keep: it carries its name as a bare
``str`` and emits no ``ast.Name`` node, so forcing ``_TYPE_VAR`` to ``()`` on
3.14.7 turns ``T`` and ``U`` into false findings. ``ast.TypeAlias`` is defensive
by comparison -- its name *is* an ``ast.Name`` in ``Store`` context, which the
preceding branch already catches -- and is left as it stands rather than removed.
A new ``test_a_pep695_type_parameter_is_in_scope`` pins both the guards and the
behaviour, skipped below 3.12 because its fixture source cannot parse there.

Measured on real interpreters rather than simulated. 3.10.21 and 3.11.15:
5 failed, exit 1 -> 5 passed, 1 skipped, exit 0. 3.14.7: all 6 pass, exit 0.
133 passed over ``tests/project`` and ``tests/utilities/test_logging.py``, exit 0,
subtests unchanged at 487.

No changelog entry on this branch. Per the rule that no code branch touches
``CHANGELOG.md`` or anything under ``docs/source/changelog/``, this change's entry
-- and the wording correction the ``MANIFEST.in`` claim implies for the #619
entry, plus the missing ``(#570)`` and ``(#577)`` citations -- go to the shared
changelog pull request #657 instead.
The `pypi` job's `environment: release` was commented out and `conda` never had
one, so a scheduled vendor bump could publish to PyPI and Anaconda unapproved.
Four jobs are now gated, one environment per credential, and the entry says
plainly that the gate is inert until the environments carry required reviewers.

Regenerated CHANGELOG.md with `util/changelog_md.py`; `--check` exits 0.
…11, not 3.12

b2ec64b qualified the claim with the wrong version. The refusal does not start
with a 3.12 change -- it starts at 3.11, and 3.12 only reworded the message. The
cross-review on #669 caught it; I had inferred 3.12 from the message text I
happened to measure on, which is the wrong evidence for a boundary.

Measured across every interpreter available here rather than inferred, with a bare
`class Flags(enum.IntFlag): pass`:

    3.8.20   Flags(0) -> OK <Flags.0: 0>
    3.9.25   Flags(0) -> OK <Flags.0: 0>
    3.10.21  Flags(0) -> OK <Flags.0: 0>
    3.11.15  Flags(0) -> TypeError: <flag 'Flags'> has no members defined
    3.12.13  Flags(0) -> TypeError: ... has no members; specify `names=()` ...
    3.14.7   Flags(0) -> TypeError: ... has no members; specify `names=()` ...

Confirmed in CPython's source, not just behaviourally. 3.11's `enum.py:1117`, inside
`Enum.__new__`, raises `TypeError("%r has no members defined" % cls)` when
`not cls._member_map_`, and it runs *before* the `_missing_` hook that manufactured
the pseudo-member on 3.10. 3.10's `enum.py` has no such raise -- its only "has no
members" occurrence is a comment at :616. 3.11 is also where the metaclass was
renamed (`class EnumType(type)` at :479 with `EnumMeta = EnumType` at :1052, against
3.10's `class EnumMeta(type)` at :161), so "the enum rewrite" is the 3.11 release.

One word in the bullet. #669 carries the matching correction to its source comment
and to its test's `sys.version_info` gate, which had been skipping the assertion on
3.11 -- a version the unit-test matrix runs -- even though 3.11 does raise.

`CHANGELOG.md` regenerated with `util/changelog_md.py`, not edited; `--check` exits 0
and `tests/project/` is green at 96 passed, 469 subtests.

Committed from a detached HEAD on b2ec64b and pushed to the branch ref, because
`docs/changelog-1.5.0` is checked out in another agent's worktree.

Refs #652
Refs #650
The changelog changes #666 was carrying on its own branch, moved here so that #666
touches only `MANIFEST.in`, `pcapkit/protocols/schema/internet/ipv6_route.py`,
`pcapkit/utilities/logging.py` and `tests/project/test_annotation_names.py`. #666
was the last open branch still editing `CHANGELOG.md` itself; it no longer does.

Four pieces, not one, because #666 had amended existing entries as well as needing
a new one.

A new **Fixed** bullet for #642: three names used in string annotations that their
own module never imported -- `Any` in `pcapkit/utilities/logging.py`, and
`Protocol` and `Optional` in `pcapkit/protocols/schema/internet/ipv6_route.py`.
The bullet names the `typing.cast` case specifically, because that is the one no
running test can catch: `cast` never evaluates its first argument. It states
plainly that nothing resolves at runtime that did not before, since `TYPE_CHECKING`
is `False` when the interpreter runs, so that the entry is not read as a runtime
fix. mypy 2.3.1's before/after is quoted as the measurement -- three `name-defined`
errors to none, 115 total to 112 -- and the new
`tests/project/test_annotation_names.py` is described as what pins the invariant
without a type checker installed.

Three missing citations recovered: `(#570)` on the L2TPv3 worked-example line,
`(#577)` on the `register_extractor_engine` keyword line, and `(#619)` on the
README rename entry.

And the #619 entry's packaging claim corrected in place. It asserted that
`include README.md` in `MANIFEST.in` was "the only thing that puts the README in a
source distribution" and that an sdist without it "cannot be installed". Both
halves are false: setuptools' own `sdist` command ships the README before
`MANIFEST.in` is read at all, so dropping the line leaves the listing
byte-identical at 861 entries, and `setup.py` reads the file from wherever it is
executing, which for a `pip` install of an sdist is the unpacked sdist. #666
corrects the same claim in the `MANIFEST.in` comment, so the two stay in step.

No `:pep:` role, though the new bullet discusses PEP 695: `util/changelog_md.py`
converts only double-backtick literals and the `:rfc:` role, and raises
`ResidualMarkupError` on anything else, exactly as the #661 entry hit with `:obj:`.
Plain prose instead.

50 lines added to the two files, 11 reflowed. `CHANGELOG.md` regenerated with
`util/changelog_md.py`, not edited; `--check` exits 0 and `tests/project/` is green
at 96 passed, 469 subtests, exit 0 -- the same counts the previous commit on this
branch reported, so nothing else moved.

Committed from a detached HEAD and pushed to the branch ref, because
`docs/changelog-1.5.0` is checked out in another agent's worktree at a stale
f846523 and could not be taken here. Written against b2ec64b and rebased onto
d47ada0, the branch having taken #641's entry and the #652/#650 boundary
correction in the meantime. Both files conflicted, both at the append point rather
than in substance -- #641's bullet and this one land in the same place at the end
of **Fixed** -- so the resolution keeps both, #641's first. `CHANGELOG.md` was not
hand-resolved: it is generated, so it was regenerated from the resolved entry file
and `--check` re-run, which is the only resolution that cannot drift.

Refs #642
Closes the residual the #573 entry above records under "What this does not
close": the 16-bit band, where a shortfall of 65,536 octets or fewer is padded
unconditionally and charged to nothing, so an option declaring 65,535 octets
could be repeated per block without limit.

`util/changelog_md.py` regenerated `CHANGELOG.md`; `--check` exits 0.
Records the six constant registries that rejected an invalid value in a way the
built-in `enum` does not -- three composing a pseudo-member for any integer at
all -- and why the issue's own proposal to raise `EnumError` was rejected rather
than adopted.

Also corrects the #623 entry above, which stated that
`pcapkit/const/tcp/flags.py` "defines no `_missing_`". It defined none at the
time and never had #623's recursion defect, which is what that sentence was
about, but it has one now; the tense is fixed and the new entry named.

`CHANGELOG.md` regenerated with `python util/changelog_md.py`;
`--check` exits 0.
The entry described only the payload bound. The cross-review of #676 found that
the option *area* is sized from a Block Total Length nothing checks against the
file, so a block declaring 1,000,000 octets while holding 36 synthesised 65,535
anyway -- 1,820x, unwarned. `bounded_area` closes that and the entry now says so.

Also corrects two claims the same review disproved: the negative-remainder skip
is load-bearing on the unpacking path rather than the packing one, and the
truncation sweep is not uniformly one exception type.

`util/changelog_md.py` regenerated `CHANGELOG.md`; `--check` exits 0.
…ly fixes

#664 was narrowed per the owner's decision in #679: `LOCATOR_SET` is excluded and
keeps the pre-#651 padding expression, so the change corrects 45 of the 46 HIP
parameters rather than all of them. This entry claimed all of them, which would
have shipped a false statement about wire output in the release notes.

Three claims narrowed, in place rather than as a second bullet:

- the opening, from "every HIP parameter" to 45 of 46, naming the exclusion;
- the site counts, from "the 46 callbacks and the 49 record lengths ... instead of
  95 times" to 93 of the 95 sites, 45 of 46 and 48 of 49;
- the migration sentence, which said every other parameter's `length` moves
  likewise -- now the other 44, with `LOCATOR_SET` called out as unchanged in both
  its octets and its data model.

Added the reason for the exclusion, because a reader who meets it in the code
otherwise cannot tell it from an oversight: two defects in that parameter cancel
exactly -- the padding callback never receives the parameter's `len`, since the
nested `Locator` schemas share a packet context whose own `len` shadows it, and the
parameter's `len` is in 4-octet units where the RFC's `Length` is a byte count.
Always-4 padding gives `4 + 24n + 4`, and because `24n` is a multiple of 8 the RFC
total for a byte-count `Length` of `24n` is the same `24n + 8`. Measured at
n = 1, 2, 5 as 32, 56 and 128 octets on `b34f132f6` and on #664's head alike, so
correcting only the padding would have taken a conformant parameter to four octets
short. #679 carries the pair.

32 lines changed in the entry file; `CHANGELOG.md` regenerated with
`util/changelog_md.py`, not edited. `--check` exits 0 and `tests/project/` is green
at 96 passed, 469 subtests.

Committed from a detached HEAD on aef0fb9 and pushed fast-forward to the branch
ref, because `docs/changelog-1.5.0` is checked out in another agent's worktree and
could not be taken here.
JarryShaw added a commit that referenced this pull request Sep 22, 2026
…t values (#653, #654, #655) (#665)

`_make_param_puzzle` and `_make_param_solution` derived three wire-format
quantities from the payload value rather than taking them from the data model.
All three derivations were wrong, in the same two functions, and they are fixed
together because the width resolution is one expression that cannot be written
twice.

* The field width came from `int.bit_length()` and nothing else, so every
  leading zero octet was dropped on re-serialisation. A SOLUTION read with
  `Length = 20` rebuilt as `Length = 6`, a PUZZLE read with `Length = 12` as
  `Length = 5` -- silently, since the integers survive and nothing raises. Both
  data models now carry `rhash_len`, the field's on-wire width in bits, and both
  builders prefer it. This only became reachable end to end once #608 was fixed
  (#629); before that the undersized rebuild tripped the reader's parity guard
  first and failed loudly. (#653)
* SOLUTION's second contents octet is `Reserved`, "zero when sent, ignored when
  received" (RFC 7401 5.2.5, RFC 5201 5.2.5 identically) -- not a PUZZLE
  `Lifetime`, which only RFC 7401 5.2.4 defines. pcapkit wrote `0x20`, `0x21`,
  `0x25` or `0x2b` there and could not write the mandated zero at all: a
  conformant SOLUTION with `Reserved = 0x00` parsed to `timedelta(0)` and then
  escaped a bare `ValueError` from `math.log2(0.0)`. `Data_SolutionParameter`
  and `SolutionParameter` now carry `reserved` verbatim, so the conformant zero
  round-trips and a received non-zero octet is reproduced rather than
  re-derived. (#654)
* Neither builder read its own `version` keyword, so `version=1` and
  `version=2` computed identical lengths at every bit width. RFC 5201 5.2.4 and
  5.2.5 state both fields as literally 8 bytes and `Length` as literally 12 and
  20, and the readers enforce exactly that -- so under HIPv1 each builder
  accepted only a `bit_length()` of 57..64 and built, for everything else, a
  parameter this library's own reader rejects. Width now comes from the version
  under HIPv1. (#655)

The remaining `math.log2` sites, both in PUZZLE where a `Lifetime` really
lives, now raise `ProtocolError` rather than letting `ValueError` escape.
`ProtocolError(BaseError, ValueError)` is what the readers already raise for a
malformed PUZZLE or SOLUTION, and deriving from the builtin it replaces keeps
any caller written around today's bare `ValueError` working; `EnumError` is
`(BaseError, TypeError)` and would silently stop being caught. The same guard
covers the upper end, because `UInt8Field` wraps rather than raising --
measured, `300` packs as `0x2c` -- so an out-of-range lifetime would otherwise
be written as some other valid-looking duration. A plain `float` lifetime used
to escape an `AttributeError` from the `isinstance(lifetime, int)` branch; the
test keyed on `timedelta` instead found that.

`_make_param_solution` no longer accepts `lifetime=`. `reserved=` and
`rhash_len=` are both `None`-sentinelled, so an explicit value overrides what a
parsed parameter carries: `Data_SolutionParameter` is immutable, so without that
a caller holding a parsed parameter had no way to write the conformant zero over
a peer's non-conformant `Reserved`. The plain data fields still let `param` win,
as they did before. `rhash_len=` is also the only way a from-scratch HIPv2 build
can state a width, which genuinely varies with the Responder's HIT Suite
(RFC 7401 2.3, 5.2.10).

Five new tests, each verified to fail on 0c7f2b7 for the defect's own reason
and pass here: `12 != 5` and friends for #655, `expected a positive input` and
`'reserved' not found` for #654, `no attribute 'rhash_len'` for #653. The
widths exercised are 1, 9, 15, 17, 57 and 65 bits, where the candidate formulas
disagree, with the byte-aligned widths kept as controls -- at a multiple of 8
the correct `2 * ceil(b / 8)` coincides with #608's `ceil(b / 4)`, which is why
every byte-aligned fixture passed through that defect unharmed.

The byte-exact assertions compare the parameter without its trailing padding,
and then against the re-packed source schema rather than against a literal, so
they pin the `Length` field and the payload octets without encoding a padding
rule that #651/#664 is concurrently changing. Verified against a `git
merge-tree` of this branch and #664: both library files auto-merge with no
conflict, and all five new tests pass against the merged library.

Two cases are deliberately accepted rather than rejected, and now say so in
`_make_puzzle_field_width`'s docstring: `rhash_len == 0`, which is what the
derived path yields for the default `random=0` and what a `Length = 4`
parameter parses back to; and a `version` other than 1, which is treated as
HIPv2 exactly as both readers' `version == 1` guards do.

Coverage holds at 100% statement and branch on all three changed modules, with
statements 1385 -> 1414 and branches 316 -> 338; 30 tests / 434 subtests ->
35 / 455. Scoped unit tier green: 291 passed, 1191 subtests. No
EXPECTED_FAILURES entry moved -- still 45, with the same four HIP entries.

No changelog entry on this branch: that is consolidated in #657.
JarryShaw added a commit that referenced this pull request Sep 22, 2026
…a false packaging claim (#642) (#666)

Three names appeared in string annotations that their module never imported, and
they were mypy's complete set of ``name-defined`` findings for the package:

* ``pcapkit/utilities/logging.py:350`` used ``Any``; added to the
  ``TYPE_CHECKING`` block beside ``IO``, ``Optional`` and ``Union``.
* ``pcapkit/protocols/schema/internet/ipv6_route.py:136`` used ``Protocol`` and
  ``:271`` used ``Optional``; added, ``Protocol`` as
  ``ProtocolBase as Protocol``. Twenty-one sibling schema modules already spell
  it that way, in the same ``payload:`` stub; this makes twenty-two.

mypy 2.3.1 over ``pcapkit``: 3 ``name-defined`` errors before, 0 after; 115 total
errors before, 112 after, so nothing else moved.

``MANIFEST.in:14-17`` asserted that ``include README.md`` was "the only thing
that puts it in an sdist" and that an sdist without it "cannot be installed at
all". Both halves are false, and the file had contradicted itself since #631
wrote the correct mechanism seven lines below without correcting this. Deleting
the lines and rebuilding gives a byte-identical sdist listing -- empty diff --
that installs with exit 0: ``setuptools/command/sdist.py:59-60`` ships the README
unconditionally and ``setuptools/dist.py:460``'s default ``license_files`` glob
ships ``LICENSE``. Of the original three ``include`` lines only ``CHANGELOG.md``
is load-bearing. The comment now says that.

New ``tests/project/test_annotation_names.py`` resolves every string annotation
in the package -- following a nested forward reference such as
``'list["Nested"]'``, while treating ``Literal`` members and ``Annotated``
metadata as the values they are -- against the names its own module binds. It
reports the same three findings as mypy on the unfixed tree and none after.

That module named ``ast.TypeAlias`` and ``ast.TypeVar`` directly, and both are
PEP 695 nodes added in Python 3.12, so *every* test in it raised
``AttributeError`` on the 3.10 and 3.11 matrix jobs -- ``bound_names`` walks every
node of every file, so the attribute is reached whatever a test does. Both are now
resolved once at module scope through ``getattr(ast, ..., ())``, leaving the
``isinstance`` branches otherwise untouched: ``isinstance(x, ())`` is always
False, so the branches stay live on 3.12+ and are simply unreachable below it.
Chosen over a ``sys.version_info`` comparison because it writes no version number
down at all -- a comparison states 3.12 next to the attribute it guards, and the
two can then drift -- and over a per-node ``getattr`` because a module-level
constant lifts the lookup out of a loop that runs on every node of every file.

``ast.TypeVar`` is the branch that earns its keep: it carries its name as a bare
``str`` and emits no ``ast.Name`` node, so forcing ``_TYPE_VAR`` to ``()`` on
3.14.7 turns ``T`` and ``U`` into false findings. ``ast.TypeAlias`` is defensive
by comparison -- its name *is* an ``ast.Name`` in ``Store`` context, which the
preceding branch already catches -- and is left as it stands rather than removed.
A new ``test_a_pep695_type_parameter_is_in_scope`` pins both the guards and the
behaviour, skipped below 3.12 because its fixture source cannot parse there.

Measured on real interpreters rather than simulated. 3.10.21 and 3.11.15:
5 failed, exit 1 -> 5 passed, 1 skipped, exit 0. 3.14.7: all 6 pass, exit 0.
133 passed over ``tests/project`` and ``tests/utilities/test_logging.py``, exit 0,
subtests unchanged at 487.

No changelog entry on this branch. Per the rule that no code branch touches
``CHANGELOG.md`` or anything under ``docs/source/changelog/``, this change's entry
-- and the wording correction the ``MANIFEST.in`` claim implies for the #619
entry, plus the missing ``(#570)`` and ``(#577)`` citations -- go to the shared
changelog pull request #657 instead.
``SeekableReader.truncate()`` now raises instead of returning a size, and the
misspelled ``writeable()`` is spelled ``writable()``. Filed as **Changed**
rather than **Fixed**, following the #617 entry: both halves are breaks to a
public API, even though nothing inside the package called either method.

CHANGELOG.md regenerated with ``util/changelog_md.py``; ``--check`` exits 0.
`register_protocol` keyed the protocol registry on `cls.__name__.upper()` and
three dispatchable classes are named `HTTP`, so registering one silently
displaced another. The entry records the measurement, the correction to the
issue's `RegistryWarning` claim, why the guard departs from the presence-only
siblings, and why re-keying belongs to #514.

Regenerated CHANGELOG.md with util/changelog_md.py; --check exits 0.
Every PCAP-NG packet block lost its captured octets: they were extracted from the
block schema and then overwritten by `ProtocolBase.__init__` with
`self.packet.payload`, which the inherited `packet` had split at `PCAPNG.length`
-- the wire's Block Total Length. The entry records the measurement on the
committed `dhcp.pcapng`, the three affected block types and their three payload
offsets, why the fix belongs at `PCAPNG.packet` rather than at the injection site,
the 104-octet dump that made it a wire-format defect, why it ships labelled
breaking, and that #678 is measurably unaffected.

CHANGELOG.md regenerated with `util/changelog_md.py`; `--check` exits 0.
…ether literals

The counts in `dbcd0e9aa` were written before the tests were hardened: the file
grows 4 tests to 11 and 3 subtests to 24, not to 10 and 19. The entry now also
records the two shapes no fixture exercised -- an option area after the captured
data, which every block of `dhcp.pcapng` lacks, and a big-endian section -- and
the two ways a snapped block is expressed, since those are what make the
"every affected block type" claim more than the one type a fixture happens to
have.

Six inline literals also lost the punctuation that was attached to them:
`` ``PCAPNG.PACKET_TYPES`` , `` and five like it, plus `` ``struct`` -only ``,
from a line-wrapper that tokenised the paragraph without preserving adjacency.
Reflowed with one that does, and with the check the generator already enforces --
a literal may not span a line break -- asserted rather than discovered.

CHANGELOG.md regenerated with `util/changelog_md.py`; `--check` exits 0.
…ts coverage claim

The cross-review on a second model returned NEEDS CHANGES on three points and the
entry now reflects the code as it actually ships:

* `PCAPNG.packet` is a plain `property`, not the `cached_property` it overrides.
  Caching it made a second `unpack` on one instance hand back the first call's
  octets -- an invariant the pre-#646 code held, since it recomputed from the
  schema every call. The entry records why, and the unit test now asserts it.
* The coverage claim compared the base tree under the *old* tests against the
  branch under the new ones, which moves the suite and the library together and
  then credits the difference to either. Re-measured with the same tests on both:
  `misc/pcapng.py` 99.91% with its miss flat at 1, and `protocol.py` identical in
  every column rather than "232 to 228 misses", which is the check that its change
  really is docstring-only.
* The #678 parity claim quoted a tally without saying what "101 truncation levels"
  meant; three readings of it give three different tallies. The set is now spelled
  as the expression that produced it, and the per-level fingerprint is given, since
  a matching aggregate can hide two levels that swapped.

CHANGELOG.md regenerated with `util/changelog_md.py`; `--check` exits 0.
Records the three mis-parenthesised HTTP/2 payload length callbacks: the
`else` arm returned `0` instead of subtracting `0`, so an unpadded DATA,
HEADERS or PUSH_PROMISE frame parsed with its whole payload discarded.

Carries the measured octets rather than a description, the algebraic reason
padded frames could not have moved, the derivation that the padded arm has no
off-by-one, the three plain-form sibling sites that localise the defect to the
grouping, the four wrong fixes the new tests reject, and the `EXPECTED_FAILURES`
and coverage numbers either side. Also notes the stale `:572` reference in the
`httpv2-frame/PRIORITY` entry, whose guard is now at `:562`.

`CHANGELOG.md` regenerated with `util/changelog_md.py`; `--check` exits 0.
… its counts

A cross-review on a second model turned up two things the first #668 entry did
not say, one of them a second half to the defect:

- `BytesField` consults its `length` callback when packing as well as parsing,
  so the mis-parenthesised arm also declined to *write* the payload. An unpadded
  DATA frame packed to nine octets of header declaring 21, which is pcapkit
  emitting malformed HTTP/2 rather than only mis-reading it. The entry framed
  the defect as parse-side, following the issue; it now carries both halves and
  the measured octet counts for all three frames.
- The first revision's tests were satisfied by a `max(computed, 1)` wrong fix on
  the two `fragment` fields, because every fixture carried a non-empty payload
  and only DATA had a padding-only case. Recorded as the sixth rejected variant,
  with what it silently produced.

Counts corrected accordingly: 15 tests and 9 subtests to 23 and 12, four
failures before to seven, 137/547 to 145/550, and 73→88/399→408 to
73→96/399→411. Also records the AST sweep of all 496 package files, and why the
one other site sharing the shape (`ipv4.py:336`) is correct rather than the same
defect.

`CHANGELOG.md` regenerated with `util/changelog_md.py`; `--check` exits 0.
…ength (#678)

Code ships in #699. One hunk, 60 added lines, zero deletions, as every entry
on this branch is. `python util/changelog_md.py --check` exits 0.
…to 14

The sweep leaves 1,495 of 1,509 levels parsing, so 14 still raise, not 13:
8 StreamEOFError, 4 FormatError and 2 ProtocolError. The per-type figures in
the same sentence already summed to 14; only the total was wrong.
…ts sweep claim

- The journal export block's bare struct.error, which #699 now fixes too: the
  block's own 32-bit NUL padding was read as a binary field's name, so every
  entry of unaligned length raised. Reachable from valid input.
- The "no foreign exception" claim scoped to the truncation sweep, with the two
  families a fuzz still reaches named and measured unchanged either side: #701
  (an unassigned block type raising from aenum) and #593's 32-bit band.
- #704, the silent loss of every journal field after a binary one, noted as
  filed rather than fixed.
- Where the two remaining ProtocolError levels actually cut: the Interface
  Description Block's if_tsresol option, not the Section Header Block.

`python util/changelog_md.py --check` exits 0.
…fixed in #699

The cross-review's second pass found an OverflowError from a binary field's
64-bit length reaching BytesIO.read at 2**63 and above, and a UnicodeDecodeError
from a field name, key or value that is not UTF-8. Both pre-existing, both in
the function #699 had just fixed the struct.error in, both now clamped or
replaced and reported.

`python util/changelog_md.py --check` exits 0.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Pull requests that change documentation only (docs: subject prefix)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant