Skip to content

pcap/pcapng: fix frame bytes, output filenames, and timestamp timezones - #403

Merged
JarryShaw merged 4 commits into
mainfrom
fix/frame-bytes-filenames-timezone
Sep 16, 2026
Merged

JarryShaw merged 4 commits into
mainfrom
fix/frame-bytes-filenames-timezone

Conversation

@JarryShaw

Copy link
Copy Markdown
Owner

Closes #357, #358, #361.

#357 — bytes(frame) returned the next frame's bytes

Verified on main before touching anything, using arp.pcap (176 bytes, records at raw[24:100] and raw[100:176]):

frame[0] len= 76 expected= 76 match=False     <- returned raw[84:100], record 2's header
frame[1] len= 16 expected= 76 match=False

Root cause. read() rewound -self.length (16) with SEEK_CUR to find the record's start — but by then the schema unpack had consumed the 16-octet record header and the incl_len payload octets, since Schema_Frame.packet is a PayloadField(length=incl_len). The rewind therefore landed incl_len octets late. Nested layers were unaffected because _import_next_layer reads the schema's payload rather than self._data, which is why this survived.

Fixed the way the PCAP-NG reader already does it: record an absolute _seek_set before the unpack and seek to it with SEEK_SET. The post-frame cursor works out to the same value as before in the well-formed case, so the engine's sequential read is unchanged.

PCAP-NG does not share it — verified across all 121 blocks in six samples: every stored block's bytes() lands on a real block boundary walked out of the raw file, 0 mismatches.

#358 — files=True wrote Frame 1..json

Extractor.__output__ spells extensions with the dot ('.json'), while make_name's own docstring promises them without — and all seven consumers compose f'{name}.{ext._fext}'. The existing engine tests already set _fext: 'json' bare, so the dotted value was the minority spelling.

make_name now honours its documented contract and supplies the dot itself in the non-files branch, so no engine module needed touching. PCAP-NG shared the idiom and is fixed by the same normalisation. TraceFlow keeps a separate __output__ and composes f'{label}{self._fdpext}' with no literal dot, so it still wants the dotted form and is deliberately left alone.

#361 — PCAP-NG timestamp_epoch shifted by the host timezone

The same frame of the same committed capture, read on main:

TZ epoch drift
UTC 1102274184.317453 —
Asia/Shanghai 1102302984.317453 +28800 s
America/New_York 1102259784.317453 −14400 s

Root cause. _read_timestamp added tzone.utcoffset(None) to the epoch it returned, and _get_timezone fell back to the reading host's zone when the capture named no if_tzone. A pcapng timestamp is an offset from the UNIX epoch, so it carries no timezone term at all (draft §4.3), and §4.2 says if_tzone "SHOULD NOT be used".

Worse: dhcp_big_endian.pcapng's first raw timestamp is 0, so it came back as -14400 and round-tripped to 18446744059309551616 — a 64-bit wraparound off the negative value.

The offset term is gone and the fallback is UTC, matching how _get_resolution and _get_offset fall back to format defaults; tzone survives only as the display datetime's tzinfo. _make_timestamp's arithmetic was already correct — only its docstring promised a conversion it never did, so the docstring was fixed rather than the code.

Why this hid so well: of six samples, the three carrying an explicit if_tzone=UTC were always right; only the three without one shifted.

Checked against if_tsresol, since two overlapping errors could mask each other: all 120 Enhanced Packet Blocks across six samples, at both 10⁶ and 10⁹ resolution, are exact for epoch, datetime and round trip under two timezones. Before the fix the same check failed on exactly the three no-if_tzone samples — so the timezone term and the resolution scaling were independent.

One change beyond the filed issues

Data_Frame.time for plain PCAP was a naive local datetime — but the same function's except ValueError branch has always returned aware UTC, and toolkit/pcapng.py:244 fills the same field with an aware datetime. So the field was naive on success and aware on failure:

try:
    _time = datetime.datetime.fromtimestamp(_irat[0] / _irat[1])        # naive local
except ValueError:
    _time = datetime.datetime.fromtimestamp(0, datetime.timezone.utc)   # aware UTC

That is not a contract, it is a bug — frame.time > x worked until it hit a malformed frame. It is now aware UTC throughout: the only self-consistent option without changing the error branch instead, consistent with PCAP-NG, and correct for a value that is epoch-relative and therefore an absolute instant. UTC specifically because the PCAP format has no usable timezone — its thiszone header field is deprecated and ignored.

This is a behaviour change for callers comparing or subtracting against a naive datetime (TypeError: can't compare offset-naive and offset-aware datetimes). Nothing in-tree does, so the suite would not have caught it; it is going into the 1.5.0 behaviour-changes list.

Verification

selection baseline after
fixture-free unit tier 575 passed, 14 skipped 579 passed, 14 skipped
tests/integration 70 passed, 3 skipped 74 passed, 3 skipped
protocol *_runtime.py/*_regression.py 46 passed 47 passed

Every delta is a test added. No pre-existing assertion was re-baselined — in particular no integration assertion pins a timestamp, so nothing there had to move.

New tests proven to fail against pristine copies of the three source files. The timezone tests are the ones worth noting: on unfixed code they pass under TZ=UTC and fail under Asia/Shanghai, America/New_York and Asia/Kolkata — the concrete demonstration that a UTC-only test would have been worthless here. They also skip with an explanatory message if the host resolves every zone to the same offset, so they cannot silently degrade.

Left alone, deliberately

  • format='pcap'/'cap' raises TypeError: PCAPIO.__init__() missing 1 required keyword-only argument: 'protocol', and format='text' raises AttributeError: module 'dictdumper' has no attribute 'Text'. Both pre-existing and unfiled; excluded from the new test module with a note.
  • PCAPNG._get_local_timezone is now unused by production code but kept, with a docstring warning that it must not drive a timestamp — it reports today's offset, so a December 2004 timestamp would get EDT rather than EST. Three existing tests reference it.

Three issues, all in what an extraction hands back.

**#357 -- `Frame.packet` and `bytes(frame)` returned the next frame's bytes.**
`read()` rewound `-self.length` (16) with `SEEK_CUR` to find the record start, but
the schema unpack had by then consumed the record header *and* the `incl_len`
payload octets, so the rewind landed `incl_len` octets late. On `arp.pcap` frame 0
returned `raw[84:100]` -- the second record's header -- and frame 1 returned 16
octets instead of 76. Fixed the way the PCAP-NG reader already did it: record an
absolute `_seek_set` before the unpack and seek to it with `SEEK_SET`. The
post-frame cursor is unchanged in the well-formed case, so the engine's sequential
read is unaffected. PCAP-NG does not share the defect -- verified across 121 blocks
in six samples, every `bytes()` landing on a real block boundary.

**#358 -- `files=True` wrote `Frame 1..json`.** `Extractor.__output__` spells
extensions with the dot while `make_name`'s own docstring promises them without,
and all seven consumers compose `f'{name}.{ext._fext}'`. `make_name` now honours
its documented contract and supplies the dot itself in the non-`files` branch, so
no engine module needed touching. PCAP-NG shared the idiom and is fixed by the same
normalisation. `TraceFlow` keeps a separate `__output__` and composes without a
literal dot, so it still wants the dotted form and is left alone.

**#361 -- PCAP-NG `timestamp_epoch` was shifted by the reading host's timezone.**
`_read_timestamp` added `tzone.utcoffset(None)` to the epoch, and `_get_timezone`
fell back to the *host's* zone when the capture named no `if_tzone`. A pcapng
timestamp is an offset from the UNIX epoch and has no timezone term. On
`dhcp.pcapng` the same frame read 1102274184.317453 under UTC, +28800 under
Asia/Shanghai and -14400 under America/New_York. `dhcp_big_endian.pcapng`, whose
first raw timestamp is 0, came back negative and round-tripped to
18446744059309551616 -- a 64-bit wraparound. The offset term is gone and the
fallback is UTC; `tzone` survives only as the display datetime's `tzinfo`. This hid
because the three samples carrying an explicit `if_tzone=UTC` were always right.
Checked against `if_tsresol`: all 120 Enhanced Packet Blocks across six samples,
at both 10^6 and 10^9, are exact under two timezones -- the two errors were
independent, so fixing one masked nothing.

Beyond the filed issues, and agreed with the maintainer: `Data_Frame.time` for
plain PCAP was a naive *local* datetime while the same function's `except
ValueError` branch returned aware UTC, and `toolkit.pcapng.block2frame` fills the
same field with an aware datetime. So the field was naive on success and aware on
failure -- not a contract, a bug, and comparisons against it worked until they
didn't. It is now aware UTC throughout, which is the only self-consistent choice
and matches PCAP-NG. UTC specifically because the PCAP format carries no usable
timezone; its `thiszone` header field is deprecated and ignored. This is a
behaviour change for callers comparing against a naive datetime, and is recorded
as such for the release notes.

Verified: fixture-free unit tier 579 passed, 14 skipped (baseline 575/14);
integration 74 passed, 3 skipped (baseline 70/3); protocol runtime/regression 47
passed (baseline 46). Every delta is a test added; no pre-existing assertion was
re-baselined, and no integration assertion pins a timestamp. The new timezone
tests pass under UTC and fail under Asia/Shanghai, America/New_York and
Asia/Kolkata on the unfixed code -- a UTC-only test would have been worthless --
and skip with a message if the host resolves every zone to one offset.
Matches the majority form already used across the suite, and the one GitHub
auto-links.
@JarryShaw
JarryShaw requested a lite review from Copilot September 16, 2026 00:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Extractor.make_name can still raise KeyError for unknown fmt instead of the documented FormatError, which can leak an unintended exception to callers.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR fixes several long-standing correctness issues in the PCAP/PCAPNG readers and the extractor’s output naming logic, and adds regression tests to lock the behavior in.

Changes:

  • Fix PCAP Frame raw-byte capture (bytes(frame), frame.packet.*) by seeking from a recorded absolute record start, and make PCAP Frame.info.time consistently timezone-aware UTC.
  • Fix PCAPNG timestamp epoch handling to be timezone-independent (UTC epoch), with UTC defaults when if_tzone is absent, plus targeted multi-timezone regression tests.
  • Normalize dumper extensions to be bare (no leading dot) in Extractor.make_name to prevent files=True outputs like Frame 1..json, and add integration coverage for real on-disk filenames.
File summaries
File Description
tests/protocols/misc/test_pcapng_unit.py Updates/extends unit coverage for PCAPNG timezone/epoch behavior and adds multi-TZ regression tests.
tests/protocols/misc/pcap/test_header_frame_unit.py Adds unit tests ensuring PCAP frames retain their own record bytes and timestamps are aware UTC.
tests/protocols/misc/pcap/test_frame_runtime.py Adds runtime-path regression test through pcapkit.interface.extract for correct frame record bytes.
tests/integration/test_files_output_naming.py New integration tests asserting files=True writes filenames with exactly one dot across formats/engines.
tests/integration/_helpers.py Updates helper docstring now that doubled-dot output is fixed and covered elsewhere.
tests/foundation/test_extraction.py Updates make_name expectations for bare extensions and broadens assertions to prevent regressions.
pcapkit/protocols/misc/pcapng.py Removes timezone offset leakage into timestamp_epoch, defaults timezone to UTC, and clarifies timestamp docs.
pcapkit/protocols/misc/pcap/frame.py Fixes PCAP frame raw-byte capture by using an absolute seek origin recorded pre-unpack; makes time UTC-aware.
pcapkit/foundation/extraction.py Normalizes registered extensions to bare form and adjusts output filename composition to avoid doubled dots.
conda/build Bumps conda build number.
Review details
  • Files reviewed: 10/10 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pcapkit/foundation/extraction.py
@JarryShaw
JarryShaw merged commit 89201df into main Sep 16, 2026
24 checks passed
JarryShaw added a commit that referenced this pull request Sep 16, 2026
…reflight

Upstream `pypcap` is unusable on a current Python: 1.3.0 compiles a `pcap.c`
pre-generated by Cython 0.29.32, which does not build against the 3.12+ C API, and
it publishes no wheels. Measured with libpcap present and found: builds on 3.10 and
3.11, fails on 3.12 (`ob_digit`, `curexc_traceback`) and 3.14 (those plus
`ma_version_tag` and `_PyLong_AsByteArray` arity).

`pcap-ct` is a ctypes reimplementation that provides the same top-level `pcap`
module and works across the whole supported range. It is added as its own engine
rather than a second backend behind `pypcap`, because the two are **mutually
exclusive** -- both distributions own the import name `pcap`, and with both
installed pcap-ct wins while upstream's extension module is shadowed and
unreachable. One engine silently meaning two different implementations is exactly
the ambiguity worth avoiding.

New shared, stdlib-only `_pcap_backend.py` is the single place that answers "which
backend did I get": `hasattr(pcap, '_pcap')` distinguishes them (pcap-ct is a
package, upstream a lone extension module), and `importlib.metadata` reveals the
distribution the import did *not* resolve to. Shared deliberately -- two copies
would be two chances for the engines to disagree. Each engine exposes `.backend`
and warns when both are installed.

`unsupported_reason()` now works as a preflight across every engine, which is the
case the import test cannot cover: a dependency that installs cleanly and only
fails when used.

* `pcap`, `pcapng` -- no override; pcapkit's own parsers have no requirement.
* `dpkt`, `scapy` -- no override, measured genuinely unconstrained on 3.14.
* `pyshark` -- ceiling 3.14 plus a tshark check. It *imports* on 3.14 and fails at
  use: the call it makes, `asyncio.get_event_loop_policy().get_event_loop()`,
  returns a loop on 3.10/3.11, warns on 3.12 and raises on 3.14. 3.13 was not
  available and is inferred, flagged as such in the code.
* `pypcap`, `pcap_ct` -- wrong-backend detection; `pcap_ct` also reports a missing
  system libpcap.
* `pypcapfile` -- unchanged from #396.

The tshark check delegates to pyshark's own `get_process_path()` rather than
`shutil.which`, which is not equivalent: pyshark consults `tshark_path` in its
config first, then PATH on POSIX, both Program Files directories on Windows, and
`/Applications/Wireshark.app` on macOS, so `which` would refuse working setups. Not
cached -- the failing path measures ~287 us against 39 PATH entries and runs once
per Extractor, and caching would freeze an answer about the *environment*, so
installing Wireshark would not take effect until restart.

Three of my own earlier claims that measurement disproved, corrected in the docs
rather than left standing: the `libpcap` wheel does **not** remove the need for a
system libpcap (its published config sets `LIBPCAP = None`, sending the loader to
`find_library("pcap")`); with no libpcap, `import pcap` raises `OSError`, not
`ImportError`, so it escapes `import_test` and aborts the extraction rather than
degrading; and the PCAP-NG verdict is host-dependent -- libpcap 1.11 reads
`dhcp.pcapng` correctly while 1.5.3 silently returns nonsense timestamps, so both
engines keep rejecting PCAP-NG, now with the real reason.

Fixed while probing: a second probe after a failure reported `NameError:
'__about__' is not defined` instead of the real cause, because both `pcap-ct`'s and
`libpcap`'s `__init__` open with a non-rerunnable `from .__about__ import * ; del
__about__`. `probe()` now purges both module trees on failure.

Also moved `test_new_engine_parity.py` to `*_runtime.py`. It read four generated
captures from the unit tier -- a tier violation that #393's guard could not catch,
because it calls `sample_path(capture)` with a *variable*, which only the runtime
layer sees, and that layer is never reached on a host where the engine packages are
absent. It was the sole cause of the 5 failures in the pristine baseline below.
`examples/legacy_smoke/_engine_support.py` listed only four engines, so the timing
harness never exercised the new ones; its skip messages now consult
`unsupported_reason()` and name the real cause.

No benchmark numbers are published. They were measured, but this host is a shared
cloud desktop under load whose figures disagree with the README's existing row in
*both* directions, so mixing the two tables row-for-row would be misleading. An
engine x Python support matrix is documented instead, which is host-independent:
3.11 is the last version where every engine runs, and even there pypcap and pcap_ct
are mutually exclusive, so no single environment has all seven.

Verified after rebasing onto 89201df: engine identity asserted rather than
inferred (`__engine_name__ == 'PCAP_CT'`, 6 frames, no EngineWarning);
`unsupported_reason()` exercised for all eight engines; `files=True` naming checked
against #403's bare-`_fext` change, which this engine's per-frame naming depends on.
Unit tier 614 passed / 28 skipped / 0 failed, against a pristine-main baseline of
5 failed / 546 passed / 50 skipped.
JarryShaw added a commit that referenced this pull request Sep 16, 2026
…reflight (#405)

* engines: add the pcap_ct engine, and make unsupported_reason a real preflight

Upstream `pypcap` is unusable on a current Python: 1.3.0 compiles a `pcap.c`
pre-generated by Cython 0.29.32, which does not build against the 3.12+ C API, and
it publishes no wheels. Measured with libpcap present and found: builds on 3.10 and
3.11, fails on 3.12 (`ob_digit`, `curexc_traceback`) and 3.14 (those plus
`ma_version_tag` and `_PyLong_AsByteArray` arity).

`pcap-ct` is a ctypes reimplementation that provides the same top-level `pcap`
module and works across the whole supported range. It is added as its own engine
rather than a second backend behind `pypcap`, because the two are **mutually
exclusive** -- both distributions own the import name `pcap`, and with both
installed pcap-ct wins while upstream's extension module is shadowed and
unreachable. One engine silently meaning two different implementations is exactly
the ambiguity worth avoiding.

New shared, stdlib-only `_pcap_backend.py` is the single place that answers "which
backend did I get": `hasattr(pcap, '_pcap')` distinguishes them (pcap-ct is a
package, upstream a lone extension module), and `importlib.metadata` reveals the
distribution the import did *not* resolve to. Shared deliberately -- two copies
would be two chances for the engines to disagree. Each engine exposes `.backend`
and warns when both are installed.

`unsupported_reason()` now works as a preflight across every engine, which is the
case the import test cannot cover: a dependency that installs cleanly and only
fails when used.

* `pcap`, `pcapng` -- no override; pcapkit's own parsers have no requirement.
* `dpkt`, `scapy` -- no override, measured genuinely unconstrained on 3.14.
* `pyshark` -- ceiling 3.14 plus a tshark check. It *imports* on 3.14 and fails at
  use: the call it makes, `asyncio.get_event_loop_policy().get_event_loop()`,
  returns a loop on 3.10/3.11, warns on 3.12 and raises on 3.14. 3.13 was not
  available and is inferred, flagged as such in the code.
* `pypcap`, `pcap_ct` -- wrong-backend detection; `pcap_ct` also reports a missing
  system libpcap.
* `pypcapfile` -- unchanged from #396.

The tshark check delegates to pyshark's own `get_process_path()` rather than
`shutil.which`, which is not equivalent: pyshark consults `tshark_path` in its
config first, then PATH on POSIX, both Program Files directories on Windows, and
`/Applications/Wireshark.app` on macOS, so `which` would refuse working setups. Not
cached -- the failing path measures ~287 us against 39 PATH entries and runs once
per Extractor, and caching would freeze an answer about the *environment*, so
installing Wireshark would not take effect until restart.

Three of my own earlier claims that measurement disproved, corrected in the docs
rather than left standing: the `libpcap` wheel does **not** remove the need for a
system libpcap (its published config sets `LIBPCAP = None`, sending the loader to
`find_library("pcap")`); with no libpcap, `import pcap` raises `OSError`, not
`ImportError`, so it escapes `import_test` and aborts the extraction rather than
degrading; and the PCAP-NG verdict is host-dependent -- libpcap 1.11 reads
`dhcp.pcapng` correctly while 1.5.3 silently returns nonsense timestamps, so both
engines keep rejecting PCAP-NG, now with the real reason.

Fixed while probing: a second probe after a failure reported `NameError:
'__about__' is not defined` instead of the real cause, because both `pcap-ct`'s and
`libpcap`'s `__init__` open with a non-rerunnable `from .__about__ import * ; del
__about__`. `probe()` now purges both module trees on failure.

Also moved `test_new_engine_parity.py` to `*_runtime.py`. It read four generated
captures from the unit tier -- a tier violation that #393's guard could not catch,
because it calls `sample_path(capture)` with a *variable*, which only the runtime
layer sees, and that layer is never reached on a host where the engine packages are
absent. It was the sole cause of the 5 failures in the pristine baseline below.
`examples/legacy_smoke/_engine_support.py` listed only four engines, so the timing
harness never exercised the new ones; its skip messages now consult
`unsupported_reason()` and name the real cause.

No benchmark numbers are published. They were measured, but this host is a shared
cloud desktop under load whose figures disagree with the README's existing row in
*both* directions, so mixing the two tables row-for-row would be misleading. An
engine x Python support matrix is documented instead, which is host-independent:
3.11 is the last version where every engine runs, and even there pypcap and pcap_ct
are mutually exclusive, so no single environment has all seven.

Verified after rebasing onto 89201df: engine identity asserted rather than
inferred (`__engine_name__ == 'PCAP_CT'`, 6 frames, no EngineWarning);
`unsupported_reason()` exercised for all eight engines; `files=True` naming checked
against #403's bare-`_fext` change, which this engine's per-frame naming depends on.
Unit tier 614 passed / 28 skipped / 0 failed, against a pristine-main baseline of
5 failed / 546 passed / 50 skipped.

* docs: use auto-numbered footnotes, and keep the README plain-docutils clean

The benchmark table's footnotes were hand-numbered `[1]_`..`[4]_`, and referenced
out of order -- the table cited 3, 1, 4, 2 -- so any insertion meant renumbering
every marker and definition by hand. They are now RST auto-numbered footnotes with
labels (`[#pyshark-historical]_` and friends), which is the form already used
across `docs/source/pcapkit/const/*.rst` as auto-symbol `[*]_`.

Labelled rather than bare `[#]_` on purpose: bare auto-numbers are assigned in
reference order, so the definitions would have to stay in the table's order to read
correctly, and a reordered row would silently repoint a marker at the wrong note.
Labels make the pairing explicit and order-independent.

Labelled auto-footnotes do take their *numbers* from definition order, though, so
the definitions are also reordered to match the table -- otherwise the markers
rendered 3, 1, 4, 2, exactly the wart being removed. Verified: markers now render
1, 2, 3, 4 in both the table and the definition list, in `README.rst` and
`docs/source/index.rst`.

Also removed three Sphinx-only roles this branch had introduced into `README.rst`
-- one `:manpage:` and two `:func:` -- which `main` did not have. GitHub renders
the README with plain docutils, which knows none of them and emits a visible
`Unknown interpreted text role` error for each, the same defect #400 fixed for the
admonitions. They are now ``literals``. README again renders with zero
diagnostics under plain docutils.

* docs: auto-symbol footnotes in the Sphinx matrix, literal symbols in the README

The engine-support matrix hand-wrote its `*` and `†` markers in the cells with
loose prose beneath, so nothing linked and the symbols had to be maintained by
hand. In `docs/source/index.rst` those are now RST auto-symbol footnotes (`[*]_`),
which index themselves.

`README.rst` deliberately keeps the literal `*`/`†` characters. GitHub renders it
with plain docutils, which does not present indexed footnotes usefully, so the
markers there stay plain text -- the same reasoning that keeps admonitions out of
that file.

One constraint worth recording, because it shaped the layout: **auto-symbol
footnotes are strictly one reference per definition.** Two references against one
definition is an error (`Too many symbol footnote references: only 1 corresponding
footnote available`), and three references mint three *separate* notes rendering
`*`, `†`, `‡`. Since `*` appeared in eight cells and `†` in four, a direct
conversion would have needed twelve definitions. The markers therefore moved off
the individual cells and onto the things they actually qualify -- the `3.13` and
`3.15` column headers, and the `pyshark` row label -- which is three references,
three definitions, and no loss of precision about which verdicts are inferred.

The benchmark table's `[1]_`..`[4]_` footnotes are left numbered as they were.

Verified: README renders with zero diagnostics under plain docutils; the Sphinx
copy's three auto-symbol footnotes resolve without warnings.
@JarryShaw
JarryShaw deleted the fix/frame-bytes-filenames-timezone branch September 17, 2026 01:07
@JarryShaw JarryShaw added fix Pull requests that fix a defect (fix: subject prefix) breaking Breaks public-facing behaviour or API (apply alongside the type label) labels Sep 22, 2026
@JarryShaw JarryShaw added this to the 1.5 milestone Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Breaks public-facing behaviour or API (apply alongside the type label) fix Pull requests that fix a defect (fix: subject prefix)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Frame.packet and bytes(frame) return the next frame's bytes

2 participants