Skip to content

Commit 5e27d30

Browse files
committed
fix(ipv4): build the TS option from its default timestamp, and print a bad one with !r (#2277)
Problem: _make_opt_ts accepted only a list or a dict for timestamp, but its default is None, so a TS option made without timestamps always raised ProtocolError; the error printed the value without !r. Fix: treat None as an empty list before the type dispatch, so it builds a Timestamp-only option with `counts` empty slots; any other non-list, non-dict raises "invalid timestamp value: must be a list or a dict, not <repr>". The docstring's Raises names ProtocolError for exactly that. Test: test_ipv4_ts_default_unit pins the default option make -> parse -> rebuild byte for byte and the message text; test_ipv4_unit's timestamp=None ProtocolError case now uses 'abc'.
1 parent 7b57bb0 commit 5e27d30

4 files changed

Lines changed: 79 additions & 6 deletions

File tree

‎examples/generators/options.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,7 +541,8 @@ def _ipv4_overrides() -> 'dict[Any, dict[str, Any]]':
541541
OptionNumber.LSR: {'counts': 9},
542542
OptionNumber.RR: {'counts': 9},
543543
OptionNumber.SSR: {'counts': 9},
544-
# ``timestamp=None``, the default, is rejected outright.
544+
# The default builds an option with no timestamp in it, which is
545+
# degenerate; one is recorded.
545546
OptionNumber.TS: {'counts': 1, 'timestamp': [1]},
546547
OptionNumber.E_SEC: {'info': b'\x00'},
547548
}

‎pcapkit/protocols/internet/ipv4.py‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1700,7 +1700,7 @@ def _make_opt_ts(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_TSOptio
17001700
timestamp: list of timestamps, i.e. the slots before the pointer, or
17011701
a mapping of the addresses to their timestamps; an address may
17021702
be anything :func:`~pcapkit.corekit.fields.ipaddress.parse_ip_address`
1703-
accepts for IPv4
1703+
accepts for IPv4; :data:`None` is an empty list
17041704
remaining: raw 32-bit words for the slots at or beyond the pointer;
17051705
the slots left over up to ``counts`` are zero
17061706
**kwargs: arbitrary keyword arguments
@@ -1710,8 +1710,7 @@ def _make_opt_ts(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_TSOptio
17101710
17111711
Raises:
17121712
ProtocolError: If ``option`` is not given and ``timestamp`` is
1713-
neither a :obj:`list` nor a :obj:`dict`, which includes the
1714-
default :data:`None`.
1713+
neither :data:`None`, a :obj:`list`, nor a :obj:`dict`.
17151714
FieldValueError: If an address is a :obj:`bool` or is not a valid
17161715
IPv4 address.
17171716
@@ -1748,6 +1747,8 @@ def _make_opt_ts(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_TSOptio
17481747
else:
17491748
ts_list = []
17501749
remainder = b''
1750+
if timestamp is None:
1751+
timestamp = []
17511752
if isinstance(timestamp, list):
17521753
flag = Enum_TSFlag.Timestamp_Only # type: ignore[assignment]
17531754
counts = min(9, counts) # 9 is the maximum number of timestamps
@@ -1785,7 +1786,8 @@ def _make_opt_ts(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_TSOptio
17851786
flag = Enum_TSFlag.Prespecified_IP_with_Timestamp # type: ignore[assignment]
17861787
ts_list.append(ts)
17871788
else:
1788-
raise ProtocolError(f'{self.alias}: [OptNo {kind}] invalid timestamp value: {timestamp}')
1789+
raise ProtocolError(f'{self.alias}: [OptNo {kind}] invalid timestamp value: '
1790+
f'must be a list or a dict, not {timestamp!r}')
17891791
pointer = 5 + len(ts_list) * 4
17901792

17911793
# NOTE: Every slot of the option is written, the empty ones as zero.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# -*- coding: utf-8 -*-
2+
"""The IPv4 Timestamp option builds from its default ``timestamp``.
3+
4+
GitHub issue #2277: ``_make_opt_ts`` dispatched on ``timestamp`` being a
5+
:obj:`list` or a :obj:`dict`, but its default is :data:`None`, so a ``TS``
6+
option made without timestamps always raised ``ProtocolError``. :data:`None` is
7+
an empty list: a Timestamp-only option with ``counts`` empty slots. Any other
8+
value still raises, with the value printed by :func:`repr`.
9+
10+
Every case builds its own octets in memory and reads no capture. Classes are
11+
imported inside each test, after :func:`~tests._support.reimport_once_per_class`.
12+
13+
"""
14+
15+
import io
16+
import unittest
17+
18+
from tests._support import reimport_once_per_class
19+
20+
#: Test tier (#1958): hand-picked round-trip cases, not a sweep.
21+
__tier__ = 'ALL'
22+
23+
#: The default ``TS`` option: length 24, pointer 5, Timestamp-only, five empty slots.
24+
DEFAULT_TS = '44180500' + '00000000' * 5
25+
26+
27+
class TestIPv4TimestampDefault(unittest.TestCase):
28+
"""Pin the default TS option through make, parse and rebuild."""
29+
30+
def setUp(self) -> None:
31+
reimport_once_per_class(self)
32+
33+
def make(self, option: 'dict[str, object]') -> 'bytes':
34+
from pcapkit.const.ipv4.option_number import OptionNumber
35+
from pcapkit.protocols.internet.ipv4 import IPv4
36+
37+
return bytes(IPv4(src='10.0.0.1', dst='10.0.0.2', ttl=64, id=0,
38+
options=[(OptionNumber.TS, option)], payload=b''))
39+
40+
def test_default_timestamp_round_trips_byte_for_byte(self) -> None:
41+
from pcapkit.const.ipv4.option_number import OptionNumber
42+
from pcapkit.const.ipv4.ts_flag import TSFlag
43+
from pcapkit.protocols.internet.ipv4 import IPv4
44+
45+
for name, option in (('omitted', {}), ('None', {'timestamp': None})):
46+
with self.subTest(timestamp=name):
47+
data = self.make(option)
48+
self.assertEqual(data[20:].hex(), DEFAULT_TS)
49+
self.assertEqual(data.hex(), self.make({'timestamp': []}).hex())
50+
51+
parsed = IPv4(io.BytesIO(data), len(data))
52+
ts = parsed.info.options[OptionNumber.TS] # type: ignore[attr-defined]
53+
self.assertEqual((ts.length, ts.pointer, ts.flag, ts.timestamp),
54+
(24, 5, TSFlag.Timestamp_Only, ()))
55+
self.assertEqual(bytes(IPv4.from_data(parsed.info)).hex(), data.hex()) # type: ignore[attr-defined]
56+
57+
def test_invalid_timestamp_message_uses_repr(self) -> None:
58+
from pcapkit.const.ipv4.option_number import OptionNumber
59+
from pcapkit.protocols.internet.ipv4 import IPv4
60+
from pcapkit.utilities.exceptions import ProtocolError
61+
62+
proto = IPv4.__new__(IPv4)
63+
with self.assertRaises(ProtocolError) as ctx:
64+
proto._make_opt_ts(OptionNumber.TS, timestamp='abc') # type: ignore[arg-type]
65+
self.assertEqual(str(ctx.exception),
66+
"IPv4: [OptNo 68] invalid timestamp value: must be a list or a dict, not 'abc'")
67+
68+
69+
if __name__ == '__main__':
70+
unittest.main()

‎tests/protocols/internet/test_ipv4_unit.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1385,7 +1385,7 @@ def test_ipv4_option_constructors_cover_common_and_error_branches(self) -> None:
13851385
self.assertEqual(padded[-1].type, OptionNumber.EOOL)
13861386

13871387
with self.assertRaises(ProtocolError):
1388-
proto._make_opt_ts(OptionNumber.TS, timestamp=None)
1388+
proto._make_opt_ts(OptionNumber.TS, timestamp='abc') # type: ignore[arg-type]
13891389
with self.assertRaises(ProtocolError):
13901390
proto._make_opt_qs(OptionNumber.QS, func=99)
13911391

0 commit comments

Comments
 (0)