Skip to content

fix(pcap): seed the frame byte order under the key the reader consults (#605) - #614

Merged
JarryShaw merged 2 commits into
mainfrom
fix/605-frame-byteorder-key
Sep 22, 2026
Merged

JarryShaw merged 2 commits into
mainfrom
fix/605-frame-byteorder-key

Conversation

@JarryShaw

Copy link
Copy Markdown
Owner

pcapkit/protocols/misc/pcap/frame.py:199 seeded packet['bytesorder'] where the reader consults packet['byteorder']. One character — and the reason it survived is the interesting half: there was no big-endian .pcap in this repository at all, so the corrected path would have been exactly as untested as the broken one. Most of this change is the fixture.

Fixes #605

The defect

site key
misc/pcap/frame.py:176, in pack() packet['byteorder'] — correct
misc/pcap/frame.py:199, in unpack() packet['bytesorder'] — the typo
schema/misc/pcap/frame.py:29, in byteorder_callback packet.get('byteorder', sys.byteorder)

The callback is attached to all four record-header fields — ts_sec (:39), ts_usec (:41), incl_len (:43), orig_len (:45) — so the .get() never found the key and every one of them was read in the host's order rather than the file's. On a little-endian host reading a little-endian capture that fallback is right by coincidence, which is why every fixture and every CI runner here has always passed.

pack() eleven lines earlier spells it correctly, which is what marks this as a slip rather than a second key deliberately named.

The reported crash reproduces, and it is the second symptom

Measured on the unfixed tree against big_endian.pcap, whose first record really holds ts_sec=1500000000, ts_usec=123456, incl_len=74:

frame 1: ts_sec=3106905 ts_usec=1088553216 incl_len=1241513984 orig_len=1241513984
         time=1970-02-05T23:19:53.553216+00:00

Every field byte-swapped, and the frame dated 1970-02-05 instead of 2017-07-14. incl_len is the payload length, so that one record consumed the whole file: three frames came back as one, with no error at all. Driven through pcapkit.extract(), the read that follows is then handed a negative payload length:

File ".../pcapkit/protocols/schema/schema.py", line 826, in unpack
    payload = data.read(payload_length)
ValueError: read length must be non-negative or -1

with packet = {'bytesorder': 'big', '__length__': -1241513732, ...} in the frame — the wrong key and its consequence in one dict. So this is a correctness defect first and an availability one second, and incl_len feeding length arithmetic is what makes the silent half worse than the loud one.

The fixture, which is the deliverable that matters

New examples/generators/endian.py, wired into make_samples.py. Reproducible, deterministic, no network, no committed binary blob (examples/captures/ is gitignored, as for every other generated capture):

fixture magic exercises
big_endian.pcap a1 b2 c3 d4 big-endian, microsecond timestamps
big_endian_nanosecond.pcap a1 b2 3c 4d big-endian and nanosecond — the first fixture in the repository to take that branch of the magic-number table
little_endian.pcap d4 c3 b2 a1 the control

Three design points worth reviewing:

  • The little-endian twin is the point of the set, not a spare. It carries the same three records as big_endian.pcap — identical timestamps, identical lengths, byte-identical packet data — so the tests can assert that the container's byte order makes no difference to what is read out, rather than only that the big-endian file matches numbers written down in a test. That property is not assertable from one file alone.
  • Frames come from scapy with real headers and real checksums (verified: stated IP/ICMP checksums equal recomputed ones), as in pcap.py. The containers are packed by hand with struct, because wrpcap writes the host's byte order and offers no way to ask for the other one — and because the container's byte order is the whole subject here, so spelling it out where it can be read beats delegating it.
  • Frame 3 is captured short — 1200 octets on the wire, cut to a 96-octet snaplen, so incl_len=96 and orig_len=1200. That is what a snapshot limit really does, and frames 1 and 2 (where the two are equal) cannot show that the fields are read separately rather than one being read and used for both. It parses cleanly, decoding to Ethernet:IPv4:UDP:Raw.

Regenerating twice into a temporary directory gives identical SHA-256s, so the fixtures are reproducible rather than snapshots of one run.

Failing, then passing

tests/protocols/misc/pcap/test_frame_endian_runtime.py drives all three fixtures through extract(), and derives its expectations from the files themselves by walking each record chain with struct as well as writing them down, so a fixture regenerated into something else fails loudly instead of moving the goalposts. It asserts all four fields, time_epoch, the payload boundary and bytes(frame) per record, and that the two containers are read alike.

A unit-tier case in test_header_frame_unit.py builds a two-record big-endian capture in memory, so the regression is also caught by the fixture-free selection unit-tests.yml runs on every push — not only by the tier that needs make samples first.

Before the fix:

FF.FF                                                                 [100%]
FAILED .../test_frame_endian_runtime.py::...::test_big_endian_nanosecond_record_headers_are_read_in_the_files_byte_order
FAILED .../test_frame_endian_runtime.py::...::test_big_endian_record_headers_are_read_in_the_files_byte_order
FAILED .../test_frame_endian_runtime.py::...::test_the_two_containers_are_read_alike
FAILED .../test_header_frame_unit.py::PCAPHeaderFrameUnitTests::test_frame_header_is_read_in_the_files_byte_order
4 failed, 1 passed, 14 warnings, 3 subtests passed in 3.83s

The three fixture-backed failures are that ValueError; the in-memory one is AssertionError: 3106905 != 1500000000. The one that passes is test_little_endian_twin_is_unaffected, and it passes on both trees deliberately — that is what shows the records themselves are not the variable.

After the fix, 4 passed, 12 subtests passed, and the big-endian and little-endian files parse to identical values field for field:

big_endian.pcap  / little_endian.pcap  (identical output)
  frame 1: ts_sec=1500000000 ts_usec=123456 incl_len=74 orig_len=74   Ethernet:IPv4:ICMP
  frame 2: ts_sec=1500000001 ts_usec=654321 incl_len=66 orig_len=66   Ethernet:IPv4:UDP:Raw
  frame 3: ts_sec=1500000002 ts_usec=456789 incl_len=96 orig_len=1200 Ethernet:IPv4:UDP:Raw

Wider runs: tests/protocols/misc/ with the changelog, tier-guard and docstring-contract suites, 131 passed, 219 subtests; the full unit tier as unit-tests.yml selects it, 1284 passed, 5 skipped, 2836 subtests (18m43s). Rebased onto origin/main (4529fdb1f), one commit, and python util/changelog_md.py --check exits 0.

Also changed

byteorder_callback now records that it is the definition of the key, and that its sys.byteorder fallback — which a standalone schema genuinely needs — makes a misspelled key indistinguishable from an absent one. That is what hid this for as long as it hid, and it is the part a reader of frame.py cannot see.

Found, deliberately not fixed

Frame.read fills len and cap_len the opposite way round from the PCAP-NG reader, and this fixture is the first thing in the repository able to show it. misc/pcap/frame.py:262-263 sets len=incl_len (captured) and cap_len=orig_len (on the wire); toolkit/pcapng.py:270-271 sets len=block.original_len and cap_len=block.captured_len. So the same sliced capture read as PCAP and as PCAP-NG reports the two fields swapped relative to each other. Frame 3 makes it visible — len=96 cap_len=1200 — where every pre-existing fixture has incl_len == orig_len and cannot.

Left alone on purpose, and not pinned by the new tests either (they assert the unambiguous frame_info.* fields instead): len is passed to _decode_next_layer as the number of octets available to parse, so it is load-bearing as the captured length on the PCAP path. Straightening the names out means deciding which of the two readers is wrong and changing behaviour a caller may depend on, which is a separate change and its own issue — not something to smuggle into a one-character byte-order fix.

Minor, and also untouched: the comment at .github/workflows/unit-tests.yml:121-122 says the Scapy extra is there "for examples/generators/pcap.py and legacy.py". endian.py needs it too now. Nothing breaks — the extra is installed either way — but the comment is a line stale.

@JarryShaw

Copy link
Copy Markdown
Owner Author

✅ GOOD TO MERGE — head 0307338ebeb09500f84f2f1470e3d468a75e8852. The fixture is real, generated (not a committed blob), and independently confirmed reproducible byte-for-byte across two separate generation runs; the deliverable test independently confirmed to fail on the unfixed tree with the exact four named failures and pass with the fix. See appendix.

@JarryShaw

Copy link
Copy Markdown
Owner Author

Cross-review appendix — PR #614

Reviewer: Sonnet; PR authored on Opus 5. Reviewed at head 0307338ebeb09500f84f2f1470e3d468a75e8852 in an isolated worktree (/tmp/pcapkit-review/pr614, removed after this review). gh pr view reports mergeable: CONFLICTING, the same changelog-file rebase collision seen on every PR in this wave — not a code issue.

Fixes keyword and CI

closingIssuesReferences = [605]. CI: rollup PENDING, CheckRun tally 7 SUCCESS, 2 SKIPPED, rest QUEUED, 0 FAILURE/CANCELLED.

The one-character fix

pcapkit/protocols/misc/pcap/frame.py:199 (inside unpack()): packet['bytesorder']packet['byteorder']. Confirmed pack() (line 176) already used the correct key on main, and schema/misc/pcap/frame.py's byteorder_callback reads packet.get('byteorder', sys.byteorder) — the asymmetry between the two methods is exactly what marks this a slip.

The fixture — checked it exists, is generated, and is reproducible

examples/generators/endian.py exists, is wired into make_samples.py's GENERATORS tuple, and writes into the gitignored examples/captures/ directory like every other generated fixture in this repo (not a committed binary blob). Generated all three fixtures (big_endian.pcap, big_endian_nanosecond.pcap, little_endian.pcap) and separately called endian.generate() again into a second, independent output directory:

big_endian.pcap:            df2b4205a66015a3800b2bebfc904f48509a1aaa999abdca39af3080bd175674   (both runs)
big_endian_nanosecond.pcap: 57584a67921aa629f7cd09dbde6777958102f63665beaa53b77c642be1654a9e   (both runs)
little_endian.pcap:         db48295a975c0e029a6aa3dda15d715634aa688b57d9d8bea43ad611bfbd80da   (both runs)

Identical SHA-256 across two independent generations — the reproducibility claim holds.

The deliverable test — fails on the unfixed tree, exactly as claimed

Reverted only line 199 (the unpack() typo site, leaving pack()'s already-correct line untouched) and ran test_frame_endian_runtime.py + test_header_frame_unit.py:

4 failed, 5 passed, 14 warnings, 3 subtests passed in 7.60s   (exit 1)

The four failures are exactly the ones the PR names: test_big_endian_nanosecond_record_headers_are_read_in_the_files_byte_order, test_big_endian_record_headers_are_read_in_the_files_byte_order, test_the_two_containers_are_read_alike, and test_frame_header_is_read_in_the_files_byte_order. Restored the fix, reran: 9 passed, 12 subtests passed, exit 0 — the subtest count (12) matches the PR's claim exactly; my "passed" count differs from their narrower "4 passed" only because I ran both whole test files rather than isolating the four regression cases. The in-memory unit-tier case (test_frame_header_is_read_in_the_files_byte_order, built without any fixture) also fails on the unfixed tree, confirming the regression is caught by the fixture-free selection too, not only by the tier that needs make_samples.py first.

The disclosed len/cap_len field-swap finding — confirmed by direct code comparison

Read both readers side by side:

# pcapkit/protocols/misc/pcap/frame.py (PCAP)
len=_ilen,       # incl_len -- captured length
cap_len=_olen,   # orig_len -- length on the wire

# pcapkit/toolkit/pcapng.py (PCAP-NG)
len=block.original_len,     # wire length
cap_len=block.captured_len, # captured length

Confirmed: the two readers populate len/cap_len with the opposite fields relative to each other. Genuinely outside this PR's scope (fixing it means deciding which reader is "wrong" and changing behavior a caller may depend on — len feeds _decode_next_layer's available-octet count on the PCAP path), and correctly not folded into a one-character byte-order fix.

Regression

tests/protocols/misc/: 52 passed, 151 subtests passed, exit 0 (a subset of the PR's combined 131 passed, 219 subtests claim, which also includes the changelog/tier-guard/docstring-contract suites I did not rerun separately — no reason found to doubt those). python util/changelog_md.py --check exits 0.

Not independently checked

  • The full unit-tests.yml-equivalent selection (1284 passed, 5 skipped, 2836 subtests, 18m43s) was not rerun — far too large for this repo's host-safety rule (never run over the whole tree), and the scoped runs above are sufficient to confirm the fix and its regression coverage.
  • Did not independently verify the IP/ICMP checksum claim ("real headers and real checksums... stated equal recomputed") on the scapy-built frames inside endian.py.
  • The stale unit-tests.yml:121-122 comment (Scapy extra's stated purpose) was read, confirmed present, not otherwise investigated — correctly disclosed as untouched and harmless.

Disagreement log

None. Every claim checked — the fix, the fixture's reproducibility, the exact failing-then-passing test set, and the len/cap_len disclosure — held up exactly under independent reproduction.

#605)

Reading a big-endian classic PCAP byte-swapped every record header field, and
then crashed. `Frame.unpack` seeded the file's declared byte order as
`packet['bytesorder']` (misc/pcap/frame.py:199) where `byteorder_callback`
(schema/misc/pcap/frame.py:29) reads `packet['byteorder']`, so the `.get()`
never found the key and always fell back to `sys.byteorder` -- the reading
host's order rather than the file's. On a little-endian host reading a
little-endian capture that fallback gives the right answer by coincidence, and
every capture in this repository was little-endian, so the wrong code path has
always produced correct results.

- One character in `Frame.unpack`. The sibling `Frame.pack` eleven lines
  earlier already spelled the key correctly, which is what marks this as a slip
  rather than a second key deliberately named.
- `byteorder_callback` now records that it is the definition of the key and
  that its `sys.byteorder` fallback -- which a standalone schema needs -- makes
  a misspelled key indistinguishable from an absent one. That is what hid this
  for as long as it hid.
- New `examples/generators/endian.py`, wired into `make_samples.py`, because
  there was no big-endian `.pcap` here at all and a one-character fix with no
  fixture leaves the corrected path exactly as untested as the broken one. It
  writes `big_endian.pcap` (magic a1 b2 c3 d4), `big_endian_nanosecond.pcap`
  (a1 b2 3c 4d, the first fixture to take that branch of the magic-number
  table) and `little_endian.pcap` (d4 c3 b2 a1), the microsecond pair carrying
  byte-identical records in the two containers so the tests can assert that the
  byte order makes no difference to what is read out. Frames come from scapy
  with real checksums; the containers are packed with `struct`, since
  `wrpcap` writes the host's order and offers no way to ask for the other.
  Frame 3 is captured short -- 1200 octets cut to a 96-octet `snaplen` -- so
  `incl_len` and `orig_len` differ, which frames 1 and 2 cannot show.
- New `tests/protocols/misc/pcap/test_frame_endian_runtime.py` drives all three
  through `extract()` and walks each file's record chain with `struct` to derive
  its own expectations rather than trusting the numbers it also writes down. A
  unit-tier case in `test_header_frame_unit.py` builds a two-record big-endian
  capture in memory instead, so the regression is caught by the fixture-free
  selection CI runs on every push, not only by the tier that needs
  `make samples` first.

Measured on the unfixed tree, `big_endian.pcap` frame 1 -- really `ts_sec`
1500000000, `ts_usec` 123456, `incl_len` 74 -- read `ts_sec=3106905`,
`ts_usec=1088553216`, `incl_len=1241513984`, dated 1970-02-05 rather than
2017-07-14, and `incl_len` being the payload length, that one record consumed
the whole file: three frames became one. Through `extract()` the read that
followed was handed a negative payload length and raised
`ValueError: read length must be non-negative or -1` from schema.py:826, which
is the crash the report describes -- the second symptom, not the first.

New tests proven to fail without the fix: 4 failed, 1 passed, the three
fixture-backed failures by that `ValueError` and the in-memory one by
`AssertionError: 3106905 != 1500000000`. All 5 pass with it, and the
little-endian twin is the one that passes either way, which is what shows the
records themselves are not the variable. After the fix the big-endian and
little-endian files parse to identical values, field for field.
tests/protocols/misc/ plus the changelog, tier-guard and docstring-contract
suites: 131 passed, 219 subtests. Full unit tier (the selection
unit-tests.yml runs): 1284 passed, 5 skipped, 2836 subtests. Fixtures verified
to regenerate byte-identically.

Fixes #605
@JarryShaw
JarryShaw force-pushed the fix/605-frame-byteorder-key branch from 0307338 to 401fce2 Compare September 22, 2026 02:41
@JarryShaw
JarryShaw merged commit c260f70 into main Sep 22, 2026
24 checks passed
@JarryShaw
JarryShaw deleted the fix/605-frame-byteorder-key branch September 22, 2026 03:14
@JarryShaw

Copy link
Copy Markdown
Owner Author

✅ GOOD TO MERGE — head 401fce26a010d8622839fa16fd1e6cdaa770d3df. This re-points the earlier verdict onto the rebased head: the six files carrying the actual fix are byte-for-byte identical to the originally-reviewed 0307338eb, and the deliverable tests reproduce the identical 9 passed / 12 subtests passed / exit 0 shape against fixtures whose SHA-256s match the recorded values. See appendix.

@JarryShaw

Copy link
Copy Markdown
Owner Author

Cross-review appendix — PR #614 (re-point)

Reviewer: Sonnet; PR authored on Opus 5. This re-points the **✅ GOOD TO MERGE** verdict originally given at head 0307338ebeb09500f84f2f1470e3d468a75e8852 onto the current head 401fce26a010d8622839fa16fd1e6cdaa770d3df, after a changelog-only rebase.

The six source files are byte-identical between the two heads

git diff 0307338eb..401fce26a -- \
  pcapkit/protocols/misc/pcap/frame.py \
  pcapkit/protocols/schema/misc/pcap/frame.py \
  examples/generators/endian.py \
  examples/generators/make_samples.py \
  tests/protocols/misc/pcap/test_frame_endian_runtime.py \
  tests/protocols/misc/pcap/test_header_frame_unit.py

Zero bytes of output. All six paths confirmed present at the new head first, and the same command form over CHANGELOG.md returns 16475 bytes — so the empty result is a real "no change", not a pathspec that silently matched nothing. The one-character bytesorderbyteorder fix, the byteorder_callback schema change, the fixture generator, its make_samples.py wiring, and both deliverable test files are all unchanged.

Why the raw head-to-head diff looks alarming, and why it is not

git diff 0307338eb..401fce26a --stat reports 137 files changed, 2306 insertions, 278 deletions — roughly 135 pcapkit/const/*, pcapkit/vendor/* and four tests/* files carrying real content. None of that is the rebase touching this PR's diff; it is main having moved underneath it. Confirmed by parent: 0307338eb's base was 4529fdb1f, 401fce26a's is a2be2cc1a, three mainline commits later (the const/get()-lookup and ILNP-nonce fixes, #596/#607/#609). git merge-base against main returns exactly those two commits for the two heads.

Comparing each head against its own base instead gives an exact match:

git diff 4529fdb1f..0307338eb --stat   # old base -> old head
git diff a2be2cc1a..401fce26a --stat   # new base -> new head

Both produce the identical 8-file list — CHANGELOG.md (+2), docs/source/changelog/1.5.0.rst (+42), endian.py (+257), make_samples.py (4 ±), frame.py (2 ±), schema/.../frame.py (+14), test_frame_endian_runtime.py (+245), test_header_frame_unit.py (+72) — 636 insertions, 2 deletions on both sides. The PR's own contribution is unchanged; only its base moved.

Regression re-run at the new head

pcapkit.__file__ asserted to resolve into the review worktree (.../worktrees/agent-ab5542a18cc066fb6/pcapkit/__init__.py) before trusting the run — necessary here because the venv's editable install otherwise resolves pcapkit to a stale main checkout. Python 3.14.7. The three fixtures were already present and their SHA-256s matched the values recorded at the original review exactly, so they were not regenerated:

big_endian.pcap:            df2b4205a66015a3800b2bebfc904f48509a1aaa999abdca39af3080bd175674
big_endian_nanosecond.pcap: 57584a67921aa629f7cd09dbde6777958102f63665beaa53b77c642be1654a9e
little_endian.pcap:         db48295a975c0e029a6aa3dda15d715634aa688b57d9d8bea43ad611bfbd80da

test_frame_endian_runtime.py + test_header_frame_unit.py: 9 passed, 5 warnings, 12 subtests passed, exit 0 (exit code read directly from the process, not from a wrapper). Identical shape to the original review — no regression from the rebase.

CI and mergeability

closingIssuesReferences = [605], unchanged. mergeable reads UNKNOWN rather than the CONFLICTING the original review saw, consistent with the rebase push having landed shortly before the check; statusCheckRollup shows workflows QUEUED with two pre-existing SKIPPED docs gates and one SUCCESS — nothing failed, nothing finished.

Not independently checked

  • The wider tests/protocols/misc/ suite and the full unit-tests.yml selection were not rerun at the new head — host-safety scoping (a full-tree run reached 41.4 GB RSS earlier today). The scoped rerun above is sufficient to show the rebase introduced no regression.
  • The len/cap_len field-swap disclosure, and the fixture's scapy/checksum construction, were confirmed once at 0307338eb and are not re-derived here — justified by the six files being proven byte-identical.
  • CI has not finished on the new head; no CI claim is being made for it.

Disagreement log

None on substance. One note for the record rather than a finding: a reader who runs the bare head-to-head git diff 0307338eb..401fce26a --stat will see 137 files and may reasonably think the rebase dragged in unrelated changes. It did not — the base-relative comparison above is the one that answers the question, and it is clean. Flagging it because that raw number is the obvious thing to check and the obvious thing to misread.

@JarryShaw

Copy link
Copy Markdown
Owner Author

Correction to the re-point above — stale head sha, and posted after merge

Two things about my previous two comments that I should state rather than leave for someone to notice:

1. They were posted after this PR had already merged. mergedAt: 2026-09-22T03:14:56Z; my comments are timestamped 03:17:55Z, just under three minutes later. So that re-point was a record, not a gate — nothing was waiting on it.

2. The head sha I cited was one commit behind the head that actually merged. I verified 401fce26a010d8622839fa16fd1e6cdaa770d3df. This merged at e8a42c1de08ac86107155865709d923b7d61b3c6 (merge commit c260f705ba9f27b08b4603dd955a239dbeaced7a).

The verdict's substance is unaffected, and here is the check rather than the assertion. e8a42c1de is a two-parent merge commit whose parents are 401fce26a — the head I verified — and 4ecac90a1 (main). It brings main in; it does not rewrite the branch. Comparing the six files that carry the fix across those two heads by git object id:

git diff 401fce26a..e8a42c1de -- <the six fix files>   ->  0 bytes
git diff 401fce26a..e8a42c1de -- CHANGELOG.md          ->  7518 bytes   (positive control)

The control matters: an empty diff can also mean a pathspec silently matched nothing, so it is shown here to prove the command form does detect a real difference. The one-character bytesorderbyteorder fix, the byteorder_callback schema change, the endian.py generator, its make_samples.py wiring, and both deliverable test files are byte-identical between the head I reviewed and the head that merged.

So ✅ GOOD TO MERGE holds for what actually landed — but it should have named e8a42c1de, and I am flagging the stale sha rather than quietly leaving a verdict pointing at the wrong commit.

JarryShaw added a commit that referenced this pull request Sep 22, 2026
…one (#618)

BREAKING CHANGE to a public attribute. The PCAP and PCAP-NG readers filled
these two from opposite wire fields, so `frame.len` meant the captured length
out of a `.pcap` and the on-wire length out of a `.pcapng`.

* `Frame.read` wrote `len=incl_len, cap_len=orig_len` and now writes
  `len=orig_len, cap_len=incl_len`, matching `toolkit.pcapng.block2frame`. Code
  reading either attribute from a `.pcap` now gets the other field's value, and
  for a truncated frame that is a different number rather than a relabelling.
* Which reader to move was a decision, not a typo fix: the PCAP spelling is the
  older of the two (`c43892af`, 2022-01-11, docstrings agreeing a day later)
  and the PCAP-NG one arrived 15 months later (`25f216f4`). Both were
  self-consistent. Wireshark's `packet-frame.c` breaks the tie -- `frame.len` is
  "Frame length on the wire", `frame.cap_len` is "Frame length stored into the
  capture file", and `frame_len < cap_len` raises `frame.len_lt_caplen`,
  `PI_MALFORMED` -- so the later spelling is the one that fits the names.
* The data model documented the inverted meanings; its docstrings now match.
* `_decode_next_layer` is handed `frame.cap_len`, not `frame.len`: it needs the
  octets present, which is the value it already got, so dissection is unchanged.
  No test pins that line and the comment there records why.
* New `test_frame_length_runtime.py` covers both readers on the only frames
  that can tell the two fields apart -- the truncated ones #614 added.

`tests/protocols/misc/ tests/toolkit/ tests/dumpkit/` passes 116 tests and 234
subtests; `tests/foundation/ tests/integration/` passes 315 and 489. The new
module is 6 tests and 28 subtests, and fails on `main` with `96 != 1200`.

Fixes #618
@JarryShaw JarryShaw added the fix Pull requests that fix a defect (fix: subject prefix) label Sep 22, 2026
JarryShaw added a commit that referenced this pull request Sep 22, 2026
…one (#618)

BREAKING CHANGE to a public attribute. The PCAP and PCAP-NG readers filled
these two from opposite wire fields, so `frame.len` meant the captured length
out of a `.pcap` and the on-wire length out of a `.pcapng`.

* `Frame.read` wrote `len=incl_len, cap_len=orig_len` and now writes
  `len=orig_len, cap_len=incl_len`, matching `toolkit.pcapng.block2frame`. Code
  reading either attribute from a `.pcap` now gets the other field's value, and
  for a truncated frame that is a different number rather than a relabelling.
* Which reader to move was a decision, not a typo fix: the PCAP spelling is the
  older of the two (`c43892af`, 2022-01-11, docstrings agreeing a day later)
  and the PCAP-NG one arrived 15 months later (`25f216f4`). Both were
  self-consistent. Wireshark's `packet-frame.c` breaks the tie -- `frame.len` is
  "Frame length on the wire", `frame.cap_len` is "Frame length stored into the
  capture file", and `frame_len < cap_len` raises `frame.len_lt_caplen`,
  `PI_MALFORMED` -- so the later spelling is the one that fits the names.
* The data model documented the inverted meanings; its docstrings now match.
* `_decode_next_layer` is handed `frame.cap_len`, not `frame.len`: it needs the
  octets present, which is the value it already got, so dissection is unchanged.
  No test pins that line and the comment there records why.
* New `test_frame_length_runtime.py` covers both readers on the only frames
  that can tell the two fields apart -- the truncated ones #614 added.

`tests/protocols/misc/ tests/toolkit/ tests/dumpkit/` passes 116 tests and 234
subtests; `tests/foundation/ tests/integration/` passes 315 and 489. The new
module is 6 tests and 28 subtests, and fails on `main` with `96 != 1200`.

Fixes #618
JarryShaw added a commit that referenced this pull request Sep 22, 2026
…one (#618) (#635)

BREAKING CHANGE to a public attribute. The PCAP and PCAP-NG readers filled
these two from opposite wire fields, so `frame.len` meant the captured length
out of a `.pcap` and the on-wire length out of a `.pcapng`.

* `Frame.read` wrote `len=incl_len, cap_len=orig_len` and now writes
  `len=orig_len, cap_len=incl_len`, matching `toolkit.pcapng.block2frame`. Code
  reading either attribute from a `.pcap` now gets the other field's value, and
  for a truncated frame that is a different number rather than a relabelling.
* Which reader to move was a decision, not a typo fix: the PCAP spelling is the
  older of the two (`c43892af`, 2022-01-11, docstrings agreeing a day later)
  and the PCAP-NG one arrived 15 months later (`25f216f4`). Both were
  self-consistent. Wireshark's `packet-frame.c` breaks the tie -- `frame.len` is
  "Frame length on the wire", `frame.cap_len` is "Frame length stored into the
  capture file", and `frame_len < cap_len` raises `frame.len_lt_caplen`,
  `PI_MALFORMED` -- so the later spelling is the one that fits the names.
* The data model documented the inverted meanings; its docstrings now match.
* `_decode_next_layer` is handed `frame.cap_len`, not `frame.len`: it needs the
  octets present, which is the value it already got, so dissection is unchanged.
  No test pins that line and the comment there records why.
* New `test_frame_length_runtime.py` covers both readers on the only frames
  that can tell the two fields apart -- the truncated ones #614 added.

`tests/protocols/misc/ tests/toolkit/ tests/dumpkit/` passes 116 tests and 234
subtests; `tests/foundation/ tests/integration/` passes 315 and 489. The new
module is 6 tests and 28 subtests, and fails on `main` with `96 != 1200`.

Fixes #618
@JarryShaw JarryShaw added breaking Breaks public-facing behaviour or API (apply alongside the type label) and removed breaking Breaks public-facing behaviour or API (apply alongside the type label) labels Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Pull requests that fix a defect (fix: subject prefix)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

frame.py:199 seeds packet['bytesorder'] where readers expect 'byteorder', and no big-endian PCAP fixture exists to catch it

1 participant