Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The largest release since 1.0, and the first recorded here as it happened rather
- **Added** -- `pcapkit.utilities.logging` as a real interface: `get_logger()` for per-module children, `configure()` to set level, handler, stream, format or propagation at runtime, `reset()` to return to library-neutral, and `ensure_output()`. Seventeen modules now log under their own `__name__`, so a consumer can silence `pcapkit.foundation.registry` while keeping `pcapkit.foundation.extraction` (#384).
- **Added** -- `conflict` on the reassembly data models: absolute, inclusive ranges where two fragments claimed the same span with different bytes, which was previously lost silently on both the IP (#482) and TCP (#443, #478) paths.
- **Added** -- an end-to-end test tier (#376), sample-capture generators so a fresh clone can rebuild every fixture (#340), a Dockerised engine benchmark covering every supported Python version (#410), and registry round-trip coverage that records the entries which cannot close the cycle rather than skipping them (#440, #504).
- **Added** -- coverage for the untested half of the #431 accommodation: a TCP or IPv4 option whose declared length asks for more data than the capture actually holds, pinning that it still parses, with the short read left-padded rather than rejected. The one test #431 left behind only covers an option area with no data behind it at all; a candidate fix for #554 turned the untested half into an unwrapped `FieldValueError` while the rest of the suite stayed green (#571, #572).
- **Added** -- coverage for the untested half of the #431 accommodation: a TCP or IPv4 option whose declared length asks for more data than the capture actually holds, pinning that it still parses, with the short read zero-padded rather than rejected. The one test #431 left behind only covers an option area with no data behind it at all; a candidate fix for #554 turned the untested half into an unwrapped `FieldValueError` while the rest of the suite stayed green (#571, #572).
- **Changed** -- `pcapkit` no longer configures logging at import. It installs a `NullHandler` and sets no level, so verbosity is inherited from the application instead of being seized by whichever library was imported second; the old stderr handler stays as the `PCAPKIT_DEVMODE` opt-in. Three consequences worth knowing: the previous behaviour is `configure(logging.INFO, stream=sys.stderr)`; 38 registry and extractor `info` calls became `debug`, so those messages are invisible even at `INFO`; and the handler is no longer `logger.handlers[0]`. `verbose=` output stays on stdout and is not logging (#384).
- **Changed** -- each warning is reported once per channel, and `pcapkit` no longer inserts a `simplefilter('ignore', ...)` at the front of the process-global `warnings.filters` (#362--#364, #390). The application's own filter therefore wins now, which is the point of the change and also the sharp edge in it: under `-W error`, or pytest's `filterwarnings = error`, a pcapkit warning that used to be suppressed will raise. Suppress them deliberately with `warnings.filterwarnings('ignore', category=BaseWarning)`. `quiet=True` now means no record at any level and no longer sets `sys.tracebacklimit`, and the `pcapkit.utilities.warnings.DEVMODE` re-export is gone -- its canonical home is `pcapkit.utilities.logging`.
- **Changed** -- `layer=` and `protocol=` are honoured rather than inert. Both were read under the wrong names, so every value a caller passed was dropped into `**kwargs` and discarded; the CLI's `-L` also now validates its argument instead of accepting anything. The packet context reaches the schema layer for the first time as well, so a field the wire elides can be resolved from its enclosing packet (#404). `follow_tcp_stream` dispatches on the engine type, where both branches of the old test were dead and the native adapter ran against every engine's frames (#402).
Expand Down Expand Up @@ -64,6 +64,9 @@ This is the resolution of #548, which reported `TransType.L2TP` (115) as registe
- **Fixed** -- `httpv1`'s `_RE_METHOD` was unanchored and `re.match` anchors only at the start, so it prefix-matched, and the request-line reader then passed the whole `para1` to `Method.get` rather than the captured `method` group. Together those meant `b'Get'` matched on the single character `G`, satisfied the guard that decides a start-line is a request, and handed the entire mixed-case token to a lookup that raised on it. Fixing either half alone still gives a wrong answer -- normalising the lookup would parse `b'Get'` as `GET` off a one-character match, and passing the group would parse it as a method named `G`. The pattern is now anchored at both ends and the captured group is what is looked up, so a token that is not a method is a malformed request line rather than a mis-parsed one. Method tokens are case-sensitive per [RFC 9110 Section 9.1](https://datatracker.ietf.org/doc/html/rfc9110#section-9.1), so no `re.I` was added: `GET` parses, `Get` and `get` are rejected (#583).
- **Fixed** -- `_RE_STATUS` in the same reader carried the same unanchored prefix defect, found by auditing `_RE_METHOD`'s siblings, and it escaped as the wrong exception type. That pattern is only a guard -- the value is taken from `int(para2)` on the raw token -- so a prefix match let a malformed status past the guard and then out of `int()` uncaught, where `_read_http_header` documents `ProtocolError`. Measured: a status of `200x` raised `ValueError: invalid literal for int() with base 10: b'200x'`, and one of `2000` raised `ValueError: 2000 is not a valid StatusCode`; both are now `ProtocolError`. [RFC 9112 Section 4](https://datatracker.ietf.org/doc/html/rfc9112#section-4) gives `status-code = 3DIGIT`, exactly three, so the anchor is what the grammar already said -- the production lives in HTTP/1.1 because `status-code` is part of its `status-line`, while [RFC 9110 Section 15](https://datatracker.ietf.org/doc/html/rfc9110#section-15) covers the code semantics and the IANA registry rather than the syntax. `_RE_VERSION` was audited at the same time and is safe as it stands, because both of its call sites read the captured group rather than the raw token (#583).
- **Fixed** -- `get()`'s documented `default` was ignored on the integer path throughout the generated `pcapkit.const` tree, because `get` delegated the lookup to the enum call and `_missing_` has no access to the caller's `default` -- so `Hardware.get(99999, 0)` raised `ValueError: 99999 is not a valid Hardware` instead of returning the fallback it was handed. The integer path now consults `default` before letting the lookup error escape. `-1`, the placeholder the generated signature already carried, is what separates "no default was supplied" from "a default was supplied and should be used", so a caller that asked for no fallback still gets the error rather than a silent substitution. The sweep #584 asked for puts the scope at 110 of the 118 integer registries, not the three the issue named; the two carrying a bespoke integer fallback of their own, `pcapng` `OptionType` and `reg` `AppType`, are deliberately left alone, since neither drops a default by raising. Not reachable from wire data -- every value a wire field can carry already resolves -- so this is a contract fix rather than a parse fix. Applied to the nine vendor templates as well as the 113 generated modules, and a new test renders the shared template and compares it against the module generated from it, so a regeneration cannot quietly undo it (#584).
- **Fixed** -- `FieldBase.unpack` padded a short read on the wrong side. When a field's buffer falls short of its declared length -- the deliberate accommodation that lets a snapshot-truncated capture parse (#431) -- the shortfall was zero-filled with `rjust()`, which places the zeros at the *front*. That asserts the octets never read were the leading ones, and a short read has lost the trailing ones: the buffer ran out. It is now `ljust()`, which is correct for **both** byte orders rather than only for big-endian fields, so the correction is not byte-order-conditional. The two directions fail differently and only one was loud. Measured: one octet of a four-octet little-endian `120` read as `2013265920`, inflated by `2 ** 24`; three octets of a four-octet big-endian `0x01020304` read as `0x10203`, scaled *down* by 256. The second is the dangerous one -- a value smaller than the truth passes a sanity check, where an inflated one overruns -- which is why the big-endian half went unnoticed. A full read is untouched at every width and order, since the padding is only ever consulted when the buffer falls short (#604).
- **Fixed** -- as a consequence of the above, the unhandled `MemoryError` at `pcapkit/protocols/protocol.py:1016` on a truncated PCAP-NG capture. `dhcp_little_endian.pcapng` cut to 161, 641 or 1389 octets left a one-octet read of a little-endian 32-bit block length, which `rjust()` turned into `0x78000000` or `0x84000000` -- 1.88 to 2.06 GiB -- and which was then passed straight to `self._file.read()` as an allocation size, from a 1772-octet file. Under a 1 GiB address-space cap all three raise `MemoryError` there; with more address space the allocation succeeds and the parse goes on to fail anyway, so the visible symptom depended on how much memory the process could get. With `ljust()` the same reads report 120 and 132, no large allocation is attempted, and all three end in the ordinary, already-handled parse failure instead (#604).
- **Added** -- `FieldBaseShortReadPaddingSideTests`, ten cases over 133 subtests covering both byte orders at 2, 4 and 8 octets and at every truncation point: the two figures #604 reports as literals, the value-preserving property stated over every width and shortfall rather than as a table, the two failure directions as inequalities, a signed field, a byte-string field, and an unpack-then-pack cycle. Eight of the ten fail on the unfixed tree. The other two must pass on both and are the guard rails -- a full read at every width and order, which may not move, and a read against an entirely empty buffer, which pads to all zeros either way and is the #431 behaviour the option and list loops depend on (#604).
- **Added** -- `CITATION.cff`, citation metadata in Citation File Format 1.2.0, which GitHub renders as the repository's "Cite this repository" button and which citation managers and dependency inventories read directly. It is the machine-readable half of the attribution BSD-3-Clause already asks for, so credit carries into a paper or a bill of materials rather than depending on a reader opening `LICENSE`. Validated with `cffconvert --validate` and against the published 1.2.0 schema; `doi` and `orcid` are omitted rather than invented, since neither exists for this project today and both are checked formats, so a wrong value would still validate. The licence itself is deliberately unchanged -- still BSD-3-Clause, no `NOTICE` file, no change to its terms. Alongside it the copyright line moves from `2018-2023` to `2018-2026` -- `LICENSE` was its only occurrence in the tree, since `docs/source/conf.py` already derives its own from the current year -- and a stray `s` after the closing `DAMAGE.` of the licence text, present since the Mozilla-to-BSD relicence, is removed, so the wording now matches canonical BSD-3-Clause exactly (#615).
- **Fixed** -- reading a big-endian classic PCAP byte-swapped every record header field, and then crashed. `Frame.unpack` seeded the file's declared byte order under the key `bytesorder` where the frame schema's `byteorder_callback` reads `byteorder`, so the lookup never found it 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. Against a big-endian capture, measured before the fix, frame 1 of `big_endian.pcap` read `ts_sec=3106905`, `ts_usec=1088553216` and `incl_len=1241513984` for a record whose real values are `1500000000`, `123456` and `74`, dating the frame to 1970-02-05 rather than to 2017-07-14. `incl_len` is the payload length, so that first record then consumed the whole file and the second was read with a negative payload length, raising `ValueError: read length must be non-negative or -1` out of the schema -- which is the reported crash, and it is the *second* symptom rather than the first. The sibling `Frame.pack` eleven lines earlier spelled the key correctly, which is what marks this as a slip rather than a second key, and the fallback is what made a misspelled key indistinguishable from an absent one; `byteorder_callback` now records that it is the definition of the key and why the fallback hides a typo (#605).
- **Added** -- `examples/generators/endian.py`, and the byte-order tests that read what it writes. There was no big-endian `.pcap` in the repository at all, which is why #605 survived its own code review: the one-character fix leaves the corrected path exactly as untested as the broken one. The generator writes three captures -- `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`) -- carrying the *same three records* in each container, so the tests can assert that the 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. Frame 3 is captured short, 1200 octets on the wire cut to a 96-octet `snaplen`, so `incl_len` and `orig_len` differ and cannot both be satisfied by one byte-swapped value. `test_frame_endian_runtime.py` drives all three through `extract()` and walks each file's record chain with `struct` to derive its own expectations; a unit-tier case in `test_header_frame_unit.py` builds a two-record big-endian capture in memory instead, so the regression is also caught by the fixture-free selection CI runs on every push. All four fail on the unfixed tree -- the three fixture-backed ones by that `ValueError`, the in-memory one by `AssertionError: 3106905 != 1500000000` -- while the little-endian twin passes on both trees, which is what shows the records themselves are not the variable (#605).
Expand Down
40 changes: 39 additions & 1 deletion docs/source/changelog/1.5.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ pull requests between #326 and #509.
skipping them (#440, #504).
* **Added** -- coverage for the untested half of the #431 accommodation: a TCP
or IPv4 option whose declared length asks for more data than the capture
actually holds, pinning that it still parses, with the short read left-padded
actually holds, pinning that it still parses, with the short read zero-padded
rather than rejected. The one test #431 left behind only covers an option
area with no data behind it at all; a candidate fix for #554 turned the
untested half into an unwrapped ``FieldValueError`` while the rest of the
Expand Down Expand Up @@ -814,6 +814,44 @@ pull requests between #326 and #509.
well as the 113 generated modules, and a new test renders the shared template
and compares it against the module generated from it, so a regeneration
cannot quietly undo it (#584).
* **Fixed** -- ``FieldBase.unpack`` padded a short read on the wrong side. When a
field's buffer falls short of its declared length -- the deliberate
accommodation that lets a snapshot-truncated capture parse (#431) -- the
shortfall was zero-filled with ``rjust()``, which places the zeros at the
*front*. That asserts the octets never read were the leading ones, and a short
read has lost the trailing ones: the buffer ran out. It is now ``ljust()``,
which is correct for **both** byte orders rather than only for big-endian
fields, so the correction is not byte-order-conditional. The two directions
fail differently and only one was loud. Measured: one octet of a four-octet
little-endian ``120`` read as ``2013265920``, inflated by ``2 ** 24``; three
octets of a four-octet big-endian ``0x01020304`` read as ``0x10203``, scaled
*down* by 256. The second is the dangerous one -- a value smaller than the
truth passes a sanity check, where an inflated one overruns -- which is why the
big-endian half went unnoticed. A full read is untouched at every width and
order, since the padding is only ever consulted when the buffer falls short
(#604).
* **Fixed** -- as a consequence of the above, the unhandled ``MemoryError`` at
``pcapkit/protocols/protocol.py:1016`` on a truncated PCAP-NG capture.
``dhcp_little_endian.pcapng`` cut to 161, 641 or 1389 octets left a one-octet
read of a little-endian 32-bit block length, which ``rjust()`` turned into
``0x78000000`` or ``0x84000000`` -- 1.88 to 2.06 GiB -- and which was then
passed straight to ``self._file.read()`` as an allocation size, from a
1772-octet file. Under a 1 GiB address-space cap all three raise
``MemoryError`` there; with more address space the allocation succeeds and the
parse goes on to fail anyway, so the visible symptom depended on how much
memory the process could get. With ``ljust()`` the same reads report 120 and
132, no large allocation is attempted, and all three end in the ordinary,
already-handled parse failure instead (#604).
* **Added** -- ``FieldBaseShortReadPaddingSideTests``, ten cases over 133
subtests covering both byte orders at 2, 4 and 8 octets and at every
truncation point: the two figures #604 reports as literals, the
value-preserving property stated over every width and shortfall rather than as
a table, the two failure directions as inequalities, a signed field, a
byte-string field, and an unpack-then-pack cycle. Eight of the ten fail on the
unfixed tree. The other two must pass on both and are the guard rails -- a full
read at every width and order, which may not move, and a read against an
entirely empty buffer, which pads to all zeros either way and is the #431
behaviour the option and list loops depend on (#604).
* **Added** -- ``CITATION.cff``, citation metadata in Citation File Format
1.2.0, which GitHub renders as the repository's "Cite this repository" button
and which citation managers and dependency inventories read directly. It is
Expand Down
Loading
Loading