Skip to content

Commit cc737e5

Browse files
authored
Merge pull request #1110 from JarryShaw/fix/1093-mh-mesg-id-fraction
2 parents 7cc49e5 + 1d13483 commit cc737e5

3 files changed

Lines changed: 69 additions & 4 deletions

File tree

‎pcapkit/protocols/internet/mh.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8100,7 +8100,7 @@ def _make_opt_mesg_id(self, type: 'Enum_Option', option: 'Optional[Data_MesgIDOp
81008100

81018101
int_ts = interval.timestamp()
81028102
ts_sec = math.floor(int_ts)
8103-
ts_frc = math.ceil(((int_ts - ts_sec) * 1_000_000)) * 2**32
8103+
ts_frc = min(round((int_ts - ts_sec) * 2**32), 0xFFFF_FFFF) # 1/2**32 seconds per unit
81048104

81058105
timestamp = NTPTimestamp(seconds=ts_sec + 2_208_988_800, # 70 years
81068106
fraction=ts_frc)

‎pcapkit/protocols/schema/internet/mh.py‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
"""header schema for mobility header"""
44

55
import datetime
6-
import math
76
from typing import TYPE_CHECKING
87

98
from pcapkit.const.mh.access_type import AccessType as Enum_AccessType
@@ -782,9 +781,9 @@ def post_process(self, packet: 'dict[str, Any]') -> 'MesgIDOption':
782781
# convert timestamp to datetime
783782
# c.f., http://tickelton.gitlab.io/articles/ntp-timestamps/
784783
ts_sec = self.seconds - 2_208_988_800 # 70 years
785-
ts_usec = math.floor(self.fraction / 2**32)
784+
ts_frac = self.fraction / 2**32 # fraction of a second
786785

787-
self.timestamp = datetime.datetime.fromtimestamp(ts_sec + ts_usec, tz=datetime.timezone.utc)
786+
self.timestamp = datetime.datetime.fromtimestamp(ts_sec + ts_frac, tz=datetime.timezone.utc)
788787

789788
return self
790789

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from __future__ import annotations
2+
3+
import datetime
4+
import unittest
5+
from unittest import mock
6+
7+
from tests._support import reimport_once_per_class
8+
9+
EPOCH = 1_700_000_000
10+
11+
12+
class MHMesgIDFractionUnitTests(unittest.TestCase):
13+
"""The Message ID option carries the NTP fraction as a 32-bit binary fraction."""
14+
15+
def setUp(self) -> None:
16+
reimport_once_per_class(self)
17+
18+
def roundtrip(self, interval: 'datetime.datetime') -> 'tuple[int, datetime.datetime]':
19+
from pcapkit.const.mh.option import Option
20+
from pcapkit.protocols.internet.mh import MH
21+
from pcapkit.protocols.schema.internet.mh import MesgIDOption
22+
23+
proto = object.__new__(MH)
24+
made = proto._make_opt_mesg_id(Option.MESG_ID_OPTION_TYPE, interval=interval)
25+
read = MesgIDOption.unpack(made.pack())
26+
self.assertEqual(read.fraction, made.fraction)
27+
return made.fraction, read.timestamp
28+
29+
def test_fraction_roundtrip(self) -> None:
30+
for usec, fraction in ((0, 0), (500_000, 2**31), (999_999, None)):
31+
with self.subTest(usec=usec):
32+
interval = datetime.datetime.fromtimestamp(EPOCH, tz=datetime.timezone.utc).replace(microsecond=usec)
33+
packed, timestamp = self.roundtrip(interval)
34+
if fraction is not None:
35+
self.assertEqual(packed, fraction)
36+
self.assertAlmostEqual(packed / 2**32, usec / 1_000_000, delta=1e-6)
37+
self.assertEqual(timestamp, interval)
38+
39+
def test_exact_integer_second(self) -> None:
40+
interval = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
41+
packed, timestamp = self.roundtrip(interval)
42+
self.assertEqual(packed, 0)
43+
self.assertEqual(timestamp, interval)
44+
45+
def test_fraction_clamped_to_field_width(self) -> None:
46+
from pcapkit.const.mh.option import Option
47+
from pcapkit.protocols.internet.mh import MH
48+
49+
proto = object.__new__(MH)
50+
interval = mock.Mock(timestamp=lambda: 1 - 2**-40) # rounds up to 2**32
51+
made = proto._make_opt_mesg_id(Option.MESG_ID_OPTION_TYPE, interval=interval)
52+
self.assertEqual(made.fraction, 0xFFFF_FFFF)
53+
self.assertEqual(made.seconds, 2_208_988_800)
54+
55+
def test_read_fraction_is_seconds(self) -> None:
56+
from pcapkit.const.mh.option import Option
57+
from pcapkit.protocols.schema.internet.mh import MesgIDOption
58+
59+
raw = MesgIDOption(type=Option.MESG_ID_OPTION_TYPE, length=8,
60+
seconds=EPOCH + 2_208_988_800, fraction=2**30).pack()
61+
self.assertEqual(MesgIDOption.unpack(raw).timestamp,
62+
datetime.datetime.fromtimestamp(EPOCH + 0.25, tz=datetime.timezone.utc))
63+
64+
65+
if __name__ == '__main__':
66+
unittest.main()

0 commit comments

Comments
 (0)