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 pcapkit/foundation/engines/dpkt.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,10 @@ def __iter__(self) -> 'Iterator[tuple[float, bytes]]':
if nanosecond:
yield DecimalTimestamp(value, resolution, hdr.len), buf
else:
exact = decimal.Decimal(hdr.tv_sec) + decimal.Decimal(hdr.tv_usec) / resolution
# at the default engine's precision, not the caller's, which
# may be too low to hold it (:issue:`1668`)
with localcontext(prec=64):
exact = decimal.Decimal(hdr.tv_sec) + decimal.Decimal(hdr.tv_usec) / resolution
yield Timestamp(value, exact, resolution, hdr.len), buf

return _Reader
Expand Down
40 changes: 34 additions & 6 deletions pcapkit/toolkit/dpkt.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ class RecordTimestamp:
:class:`DecimalTimestamp` instead, which *is* the value `DPKT`_ yields --
so a stored frame's timestamp is exactly what it was before -- and also
carries those three. Reassembly and flow tracing are handed a plain
:obj:`float` of it, as by every other engine (:issue:`1501`).
:obj:`float` of it, as by every other engine (:issue:`1501`): of its exact
value, since `DPKT`_'s own for a microsecond PCAP rounds twice
(:issue:`1668`; see :func:`_capture_time`).

A copy, a deep copy and a pickle at any protocol all rebuild a timestamp
from what its :meth:`~object.__reduce__` returns, so all keep the three.
Expand Down Expand Up @@ -272,6 +274,29 @@ def packet2timestamp(packet: 'Packet') -> 'float':
return cast('float', timestamp)


def _capture_time(timestamp: 'float | Decimal') -> 'float':
"""The capture timestamp reassembly and flow tracing are handed.

Args:
timestamp: Capture timestamp of the packet.

Returns:
The :obj:`float` nearest the exact timestamp, as the default engine hands
over. `DPKT`_ reads a microsecond PCAP's as ``tv_sec + tv_usec / 1e6``,
which rounds twice, so can be an ulp off at a small ``tv_sec``:
``1.0036909999999999`` for ``1.003691`` (:issue:`1668`). A
:class:`RecordTimestamp` is therefore read by its exact value. Any other
timestamp -- one passed by hand, or yielded by a ``dpkt`` whose reader
the engine could not extend -- carries none, and is handed on as it is.

.. _DPKT: https://dpkt.readthedocs.io

"""
if isinstance(timestamp, RecordTimestamp):
return float(timestamp.exact)
return float(timestamp)


def attach_buffer(packet: 'Packet', buffer: 'bytes') -> 'None':
"""Stash a frame's record octets on the frame.

Expand Down Expand Up @@ -1153,7 +1178,10 @@ def ipv4_reassembly(packet: 'Packet', timestamp: 'float', *,
:func:`packet2timestamp`, which is where
:class:`~pcapkit.foundation.engines.dpkt.DPKT` leaves it. Handed on
as a :obj:`float`, as every engine's adapters do, though `DPKT`_ reads
a nanosecond PCAP's as a :class:`~decimal.Decimal` (:issue:`1501`).
a nanosecond PCAP's as a :class:`~decimal.Decimal` (:issue:`1501`),
and as the one nearest the exact value it carries, if any, where
`DPKT`_'s own may be an ulp off (:issue:`1668`; see
:func:`_capture_time`).
count: Packet index. If not provided, default to ``-1``.

Returns:
Expand Down Expand Up @@ -1210,7 +1238,7 @@ def ipv4_reassembly(packet: 'Packet', timestamp: 'float', *,
tl=int.from_bytes(octets[2:4], 'big'), # total length, header includes
header=octets[:ihl], # raw bytes type header
payload=bytearray(octets[ihl:]), # raw bytearray type payload
timestamp=float(timestamp), # capture timestamp
timestamp=_capture_time(timestamp), # capture timestamp
)
return data

Expand Down Expand Up @@ -1291,7 +1319,7 @@ def ipv6_reassembly(packet: 'Packet', timestamp: 'float', *,
tl=fragment + len(payload), # total length, header includes
header=octets[:fragment], # raw bytes type header before IPv6-Frag
payload=bytearray(payload), # raw bytearray type payload after IPv6-Frag
timestamp=float(timestamp), # capture timestamp
timestamp=_capture_time(timestamp), # capture timestamp
)
return data

Expand Down Expand Up @@ -1351,7 +1379,7 @@ def tcp_reassembly(packet: 'Packet', timestamp: 'float', *,
first=segment.seq, # first sequence number of payload
last=segment.seq + raw_len - 1, # last sequence number of payload
len=raw_len, # payload length, header excludes
timestamp=float(timestamp), # capture timestamp
timestamp=_capture_time(timestamp), # capture timestamp
)
return data
return None
Expand Down Expand Up @@ -1405,7 +1433,7 @@ def tcp_traceflow(packet: 'Packet', timestamp: 'float', *,
dst=segment.dst, # destination IP
srcport=segment.srcport, # TCP source port
dstport=segment.dstport, # TCP destination port
timestamp=float(timestamp), # timestamp
timestamp=_capture_time(timestamp), # timestamp
seq=segment.seq, # TCP sequence number
ack=segment.ack, # TCP acknowledgement number
header=segment.header, # raw bytes type header
Expand Down
10 changes: 8 additions & 2 deletions pcapkit/toolkit/pypcapfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,19 +235,25 @@ def packet2timestamp(packet: 'Packet') -> 'float':
packet: PyPCAPFile packet.

Returns:
Timestamp of the packet, in seconds since the epoch.
Timestamp of the packet, in seconds since the epoch: the :obj:`float`
nearest the exact value, as the default engine hands over.

Note:
`PyPCAPFile`_ keeps the two halves of the per-packet timestamp apart, and
the sub-second half is nanoseconds rather than microseconds when the
savefile carries the nanosecond magic number -- which is recorded on the
savefile header as ``ns_resolution``.

The two are joined in one integer division, which rounds once.
``timestamp + timestamp_us / divisor`` rounds twice, so can be an ulp
off at a small ``timestamp``: ``1.0036909999999999`` for ``1.003691``
(:issue:`1668`).

.. _PyPCAPFile: https://github.com/kisom/pypcapfile

"""
divisor = 1_000_000_000 if packet.header[0].ns_resolution else 1_000_000
return packet.timestamp + packet.timestamp_us / divisor
return (packet.timestamp * divisor + packet.timestamp_us) / divisor


def _parse_ipv4_address(value: 'Any') -> 'IPv4Address':
Expand Down
Loading
Loading