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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Preceded by `1.5.0a1` (2026-09-15), `1.5.0b1` to `1.5.0b3` (all 2026-09-19) and

#### Changed

- `Probe`, `CipherSuite` and `IntegritySuite` are `Info` subclasses rather than `typing.NamedTuple`, and no `NamedTuple` remains in the package. They are Mappings now, so `len()` and iteration yield field names rather than values.
- `Probe`, `CipherSuite` and `IntegritySuite` are `Info` subclasses rather than `typing.NamedTuple`. They are Mappings now, so `len()` and iteration yield field names rather than values. `TCPSegment` in `pcapkit.toolkit.pcap` stays a `typing.NamedTuple`, since it is built for every TCP frame and an `Info` costs more to construct.
- `ModuleDescriptor.klass` reads an already-imported module out of `sys.modules` rather than re-entering `importlib.import_module`, since next layer dispatch resolves a descriptor there on a per-frame path. Neither a registry *hit* holding a `ModuleDescriptor` nor a *miss* writes the resolved class back: a memoised hit went stale on `importlib.reload` ([#1559](https://github.com/JarryShaw/PyPCAPKit/issues/1559)), and recording a miss in a class-level `collections.defaultdict` is the defect [#421](https://github.com/JarryShaw/PyPCAPKit/issues/421)/[#426](https://github.com/JarryShaw/PyPCAPKit/pull/426) fixed at this layer, [#425](https://github.com/JarryShaw/PyPCAPKit/issues/425)/[#428](https://github.com/JarryShaw/PyPCAPKit/pull/428) for the option, chunk and block registries, and [#555](https://github.com/JarryShaw/PyPCAPKit/issues/555)/[#560](https://github.com/JarryShaw/PyPCAPKit/pull/560) at the schema layer. A resolution now costs ~117 ns rather than ~436 ns, which is **not measurable in** `extract()` **wall clock**. The change is taken for its shape: `sys.modules` stays the only module cache, whereas a memo of the class would serve the pre-reload class after an `importlib.reload` forever and fail `isinstance` against the live one. Proposed by `@Ts-Boom` in [#563](https://github.com/JarryShaw/PyPCAPKit/pull/563); the implementation differs because that one added a second, never-invalidated cache of resolved classes ([#574](https://github.com/JarryShaw/PyPCAPKit/issues/574)).
- `SeekableReader.truncate()` raises instead of returning a size, and the misspelled `writeable()` is now `writable()`. Both break external callers; nothing inside the package calls either. `io.IOBase` documents one gate over two methods -- "If False, write() and truncate() will raise OSError" -- and this reader's `writable()` answers False, yet `truncate` returned its new size, where `open(path, 'rb').truncate()` raises. The refusal is `UnsupportedOperation('truncate')`, the same one `write` raises; pcapkit's subclasses `io.UnsupportedOperation`. The old resizing is kept as the private `_truncate_buffer`: it resizes a private lookback window, not the stream, and it is the only route to the "position sits before the window" state that `seek` and the buffered read paths must refuse, for which [#643](https://github.com/JarryShaw/PyPCAPKit/issues/643) and [#644](https://github.com/JarryShaw/PyPCAPKit/issues/644) landed tests. `writeable()` was never an override of anything, so `io`, `shutil` and third parties never saw it, and the value reported is unchanged ([#645](https://github.com/JarryShaw/PyPCAPKit/issues/645)).
- **a breaking change to** `@final`, now enforced at runtime on `Info` and `Schema`. `info_final` and `schema_final` both end `return final(cls)`, so every finalised class carried `typing.final`'s `__final__` marker and nothing read it. `@final` **alone on an** `Info` **or** `Schema` **subclass -- without** `@info_final` **or** `@schema_final` **-- now raises** `InfoError` **or** `SchemaError` **at first construction**, the class being marked final but never finalised and so carrying no generated `__init__`. `__init_subclass__` cannot catch that, since `final` applies after class creation, so the check sits in the existing one-shot `FinalisedState.NONE` branch and a finalised class pays nothing. **Deriving from a finalised class raises too**, from new `Info.__init_subclass__` and `Schema.__init_subclass__` hooks (before, only `EnumSchema` had one). `SchemaError` **is a** `ValueError`, so code catching `TypeError` around a subclass declaration will not see it. `@info_final @final` and `@final @info_final` both finalise silently; the re-entry check keys on `__finalised__` rather than `__final__`, because only the former records *the decorator* having run. `@info_final` twice warns and returns the class unchanged. `pcapkit.utilities.compat` takes `final` from `typing_extensions` below 3.11 rather than 3.8, because `typing.final` only records `__final__` from 3.11. The guard cannot fire on the library's own classes ([#778](https://github.com/JarryShaw/PyPCAPKit/issues/778)).
Expand Down
6 changes: 4 additions & 2 deletions docs/source/changelog/1.5.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -382,8 +382,10 @@ Changed
~~~~~~~

* ``Probe``, ``CipherSuite`` and ``IntegritySuite`` are ``Info`` subclasses rather
than ``typing.NamedTuple``, and no ``NamedTuple`` remains in the package. They are
Mappings now, so ``len()`` and iteration yield field names rather than values.
than ``typing.NamedTuple``. They are Mappings now, so ``len()`` and iteration yield
field names rather than values. ``TCPSegment`` in ``pcapkit.toolkit.pcap`` stays a
``typing.NamedTuple``, since it is built for every TCP frame and an ``Info`` costs
more to construct.
* ``ModuleDescriptor.klass`` reads an already-imported module out of ``sys.modules``
rather than re-entering ``importlib.import_module``, since next layer dispatch
resolves a descriptor there on a per-frame path. Neither a registry *hit* holding a
Expand Down
4 changes: 2 additions & 2 deletions docs/source/contributing/pep.rst
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,8 @@ looks like an ordinary field and is not:
48/16 fixed-point split, where NTP counts from 1900 in a 32/32 one, so reading
one as the other is wrong in the epoch and in both field widths. They have
separate types for that reason:
:class:`~pcapkit.protocols.internet.mh.PMIPv6Timestamp` and
:class:`~pcapkit.protocols.internet.mh.NTPTimestamp`.
:class:`~pcapkit.protocols.data.internet.mh.PMIPv6Timestamp` and
:class:`~pcapkit.protocols.data.internet.mh.NTPTimestamp`.

DTLS
~~~~
Expand Down
42 changes: 24 additions & 18 deletions docs/source/pcapkit/protocols/internet/mh.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@ Octets Bits Name Description
6 48 ``mh.data`` Message Data
======= ========= ================== ===============================

.. versionremoved:: 1.5.0

``pcapkit.protocols.internet.mh.NTPTimestamp``, with no alias; use
:class:`pcapkit.protocols.data.internet.mh.NTPTimestamp` instead
(:issue:`1886`).

.. versionchanged:: 1.5.0

:class:`~pcapkit.protocols.data.internet.mh.NTPTimestamp` and
:class:`~pcapkit.protocols.data.internet.mh.PMIPv6Timestamp` are
:class:`~pcapkit.corekit.infoclass.Info` data models rather than named
tuples, so they no longer index, unpack, hash or compare equal to a
:obj:`tuple`, and the dumpers write them keyed by field, as
``{"seconds": ..., "fraction": ...}``. For ``NTPTimestamp`` this is a
breaking change from 1.4.x (:issue:`1886`).

.. autoclass:: pcapkit.protocols.internet.mh.MH
:no-members:
:show-inheritance:
Expand Down Expand Up @@ -273,20 +289,6 @@ Octets Bits Name Description
Auxiliary Data
--------------

.. autoclass:: pcapkit.protocols.internet.mh.NTPTimestamp
:members:
:show-inheritance:

.. attribute:: seconds
:type: int

Seconds since 1 January 1900.

.. attribute:: fraction
:type: int

Fraction of a second.

.. autoclass:: pcapkit.protocols.internet.mh.FastBindingAcknowledgmentStatus
:members:
:undoc-members:
Expand All @@ -297,10 +299,6 @@ Auxiliary Data
:undoc-members:
:show-inheritance:

.. autoclass:: pcapkit.protocols.internet.mh.PMIPv6Timestamp
:members:
:show-inheritance:

.. autoclass:: pcapkit.protocols.internet.mh.LocalizedRoutingStatus
:members:
:undoc-members:
Expand Down Expand Up @@ -1061,6 +1059,10 @@ Data Models
:members:
:show-inheritance:

.. autoclass:: pcapkit.protocols.data.internet.mh.NTPTimestamp
:members:
:show-inheritance:

.. autoclass:: pcapkit.protocols.data.internet.mh.CGAParametersRequestOption
:members:
:show-inheritance:
Expand Down Expand Up @@ -1241,6 +1243,10 @@ Data Models
:members:
:show-inheritance:

.. autoclass:: pcapkit.protocols.data.internet.mh.PMIPv6Timestamp
:members:
:show-inheritance:

.. autoclass:: pcapkit.protocols.data.internet.mh.RestartCounterOption
:members:
:show-inheritance:
Expand Down
1 change: 1 addition & 0 deletions pcapkit/protocols/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@
'MH_MulticastAcknowledgementOption', 'MH_LMAControlledMAGParametersOption', 'MH_MAGMultipathBindingOption',
'MH_MAGIdentifierOption', 'MH_AnchoredPrefixOption', 'MH_LocalPrefixOption', 'MH_PreviousMAAROption',
'MH_ServingMAAROption', 'MH_DLIFLinkLocalAddressOption', 'MH_DLIFLinkLayerAddressOption',
'MH_NTPTimestamp', 'MH_PMIPv6Timestamp',
'MH_ContextRequest',
'MH_FlowIdentificationSuboption',
'MH_UnassignedFlowIdentificationSuboption', 'MH_PadFlowIdentificationSuboption', 'MH_BIDReferenceSuboption',
Expand Down
3 changes: 3 additions & 0 deletions pcapkit/protocols/data/internet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@
from pcapkit.protocols.data.internet.mh import MultiPrefixExtension as MH_MultiPrefixExtension
from pcapkit.protocols.data.internet.mh import NATDetectionOption as MH_NATDetectionOption
from pcapkit.protocols.data.internet.mh import NonceIndicesOption as MH_NonceIndicesOption
from pcapkit.protocols.data.internet.mh import NTPTimestamp as MH_NTPTimestamp
from pcapkit.protocols.data.internet.mh import Option as MH_Option
from pcapkit.protocols.data.internet.mh import \
PadFlowIdentificationSuboption as MH_PadFlowIdentificationSuboption
Expand All @@ -331,6 +332,7 @@
PermanentHomeKeygenTokenOption as MH_PermanentHomeKeygenTokenOption
from pcapkit.protocols.data.internet.mh import \
PerSessionBitRateAttribute as MH_PerSessionBitRateAttribute
from pcapkit.protocols.data.internet.mh import PMIPv6Timestamp as MH_PMIPv6Timestamp
from pcapkit.protocols.data.internet.mh import PreviousMAAROption as MH_PreviousMAAROption
from pcapkit.protocols.data.internet.mh import QoSAttribute as MH_QoSAttribute
from pcapkit.protocols.data.internet.mh import \
Expand Down Expand Up @@ -478,6 +480,7 @@
'MH_MulticastAcknowledgementOption', 'MH_LMAControlledMAGParametersOption', 'MH_MAGMultipathBindingOption',
'MH_MAGIdentifierOption', 'MH_AnchoredPrefixOption', 'MH_LocalPrefixOption', 'MH_PreviousMAAROption',
'MH_ServingMAAROption', 'MH_DLIFLinkLocalAddressOption', 'MH_DLIFLinkLayerAddressOption',
'MH_NTPTimestamp', 'MH_PMIPv6Timestamp',
'MH_ContextRequest',
'MH_FlowIdentificationSuboption',
'MH_UnassignedFlowIdentificationSuboption', 'MH_PadFlowIdentificationSuboption', 'MH_BIDReferenceSuboption',
Expand Down
62 changes: 60 additions & 2 deletions pcapkit/protocols/data/internet/mh.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,7 @@
from pcapkit.corekit.multidict import OrderedMultiDict
from pcapkit.protocols.internet.mh import (FastBindingAcknowledgmentStatus,
IPv6AddressPrefixCode, LMAAddressCode,
LocalizedRoutingStatus, NTPTimestamp,
PMIPv6Timestamp)
LocalizedRoutingStatus)

__all__ = [
'MH',
Expand Down Expand Up @@ -108,6 +107,8 @@
'LocalPrefixOption', 'PreviousMAAROption', 'ServingMAAROption',
'DLIFLinkLocalAddressOption', 'DLIFLinkLayerAddressOption',

'NTPTimestamp', 'PMIPv6Timestamp',

'ContextRequest',

'FlowIdentificationSuboption',
Expand Down Expand Up @@ -983,6 +984,33 @@ class AuthOption(Option):
def __init__(self, type: 'Enum_Option', length: 'int', subtype: 'Enum_AuthSubtype', spi: 'int', data: 'bytes') -> 'None': ...


@info_final
class NTPTimestamp(Data):
"""Data model for NTP timestamp format, c.f., :rfc:`1305`.

.. versionchanged:: 1.5.0

An :class:`~pcapkit.corekit.infoclass.Info` data model in this module,
rather than a named tuple in :mod:`pcapkit.protocols.internet.mh`, which
no longer binds the name (:issue:`1886`). This breaks 1.4.x code that
treats it as a tuple: it no longer indexes, unpacks, hashes, orders,
compares equal to a :obj:`tuple`, or has ``_replace`` and ``_asdict``.
:attr:`seconds` and :attr:`fraction` read as before, and it still
pickles. The dumpers write it keyed by field, as
``{"seconds": ..., "fraction": ...}``, where they wrote
``{"type": "NTPTimestamp", "value": [...]}``.

"""

#: Seconds since 1 January 1900.
seconds: 'int'
#: Fraction of a second.
fraction: 'int'

if TYPE_CHECKING:
def __init__(self, seconds: 'int', fraction: 'int') -> 'None': ...


@info_final
class MesgIDOption(Option):
"""Data model for Mobility Message Replay Protection option."""
Expand Down Expand Up @@ -1259,6 +1287,36 @@ class LinkLocalAddressOption(Option):
def __init__(self, type: 'Enum_Option', length: 'int', address: 'IPv6Address') -> 'None': ...


@info_final
class PMIPv6Timestamp(Data):
"""Data model for Proxy Mobile IPv6 timestamp format, c.f., :rfc:`5213#section-8.8`.

Note:
This is **not** an :rfc:`1305` NTP timestamp, which is why it is a type of
its own rather than a reuse of :class:`NTPTimestamp`. It counts from the
UNIX epoch rather than NTP's 1900 one, and splits its 64 bits 48/16
rather than 32/32, so each field means something different in the two.

.. versionchanged:: 1.5.0

An :class:`~pcapkit.corekit.infoclass.Info` data model in this module,
rather than the named tuple in :mod:`pcapkit.protocols.internet.mh` it
was in the 1.5.0 pre-releases (:issue:`1886`). It no longer indexes,
unpacks, hashes, orders, compares equal to a :obj:`tuple`, or has
``_replace`` and ``_asdict``. The dumpers write it keyed by field, as
``{"seconds": ..., "fraction": ...}``.

"""

#: Seconds since 1 January 1970, in the leading 48 bits.
seconds: 'int'
#: Fraction of a second, in units of 1/65536, in the trailing 16 bits.
fraction: 'int'

if TYPE_CHECKING:
def __init__(self, seconds: 'int', fraction: 'int') -> 'None': ...


@info_final
class TimestampOption(Option):
"""Data model for MH Timestamp option."""
Expand Down
63 changes: 26 additions & 37 deletions pcapkit/protocols/internet/mh.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@

.. [*] https://en.wikipedia.org/wiki/Mobile_IP#Changes_in_IPv6_for_Mobile_IPv6

.. versionremoved:: 1.5.0

``pcapkit.protocols.internet.mh.NTPTimestamp``, with no alias; use
:class:`pcapkit.protocols.data.internet.mh.NTPTimestamp` instead
(:issue:`1886`).

.. versionchanged:: 1.5.0

:class:`~pcapkit.protocols.data.internet.mh.NTPTimestamp` and
:class:`~pcapkit.protocols.data.internet.mh.PMIPv6Timestamp` are
:class:`~pcapkit.corekit.infoclass.Info` data models rather than named
tuples, so they no longer index, unpack, hash or compare equal to a
:obj:`tuple`, and the dumpers write them keyed by field, as
``{"seconds": ..., "fraction": ...}``. For ``NTPTimestamp`` this is a
breaking change from 1.4.x (:issue:`1886`).

"""
import collections
import datetime
Expand Down Expand Up @@ -231,13 +247,15 @@
from pcapkit.protocols.data.internet.mh import MultiPrefixExtension as Data_MultiPrefixExtension
from pcapkit.protocols.data.internet.mh import NATDetectionOption as Data_NATDetectionOption
from pcapkit.protocols.data.internet.mh import NonceIndicesOption as Data_NonceIndicesOption
from pcapkit.protocols.data.internet.mh import NTPTimestamp as Data_NTPTimestamp
from pcapkit.protocols.data.internet.mh import \
PadFlowIdentificationSuboption as Data_PadFlowIdentificationSuboption
from pcapkit.protocols.data.internet.mh import PadOption as Data_PadOption
from pcapkit.protocols.data.internet.mh import \
PermanentHomeKeygenTokenOption as Data_PermanentHomeKeygenTokenOption
from pcapkit.protocols.data.internet.mh import \
PerSessionBitRateAttribute as Data_PerSessionBitRateAttribute
from pcapkit.protocols.data.internet.mh import PMIPv6Timestamp as Data_PMIPv6Timestamp
from pcapkit.protocols.data.internet.mh import PreviousMAAROption as Data_PreviousMAAROption
from pcapkit.protocols.data.internet.mh import QoSAttribute as Data_QoSAttribute
from pcapkit.protocols.data.internet.mh import \
Expand Down Expand Up @@ -535,36 +553,6 @@
__all__ = ['MH']


class NTPTimestamp(collections.namedtuple('NTPTimestamp', 'seconds fraction')):
"""NTP timestamp format, c.f., :rfc:`1305`."""

__slots__ = ()

#: Seconds since 1 January 1900.
seconds: int
#: Fraction of a second.
fraction: int


class PMIPv6Timestamp(collections.namedtuple('PMIPv6Timestamp', 'seconds fraction')):
"""Proxy Mobile IPv6 timestamp format, c.f., :rfc:`5213#section-8.8`.

Note:
This is **not** an :rfc:`1305` NTP timestamp, which is why it is a type of
its own rather than a reuse of :class:`NTPTimestamp`. It counts from the
UNIX epoch rather than NTP's 1900 one, and splits its 64 bits 48/16
rather than 32/32, so each field means something different in the two.

"""

__slots__ = ()

#: Seconds since 1 January 1970, in the leading 48 bits.
seconds: int
#: Fraction of a second, in units of 1/65536, in the trailing 16 bits.
fraction: int


class FastBindingAcknowledgmentStatus(EnumLookup, IntEnum):
"""[FastBindingAcknowledgmentStatus] Fast Binding Acknowledgment Status Codes.

Expand Down Expand Up @@ -3372,7 +3360,7 @@ def _read_opt_mesg_id(self, schema: 'Schema_MesgIDOption', *,
type=schema.type,
length=self._mh_option_length(schema.length),
timestamp=schema.timestamp,
ntp_timestamp=NTPTimestamp(schema.seconds, schema.fraction),
ntp_timestamp=Data_NTPTimestamp(seconds=schema.seconds, fraction=schema.fraction),
)
return data

Expand Down Expand Up @@ -4094,7 +4082,7 @@ def _read_opt_timestamp(self, schema: 'Schema_TimestampOption', *,
1900 one, and splits the 64 bits 48/16 rather than 32/32. Reading it
as an NTP timestamp would be wrong in both the epoch and the field
widths, which is why it gets its own
:class:`~pcapkit.protocols.internet.mh.PMIPv6Timestamp`.
:class:`~pcapkit.protocols.data.internet.mh.PMIPv6Timestamp`.

Args:
schema: Parsed option schema.
Expand All @@ -4115,7 +4103,7 @@ def _read_opt_timestamp(self, schema: 'Schema_TimestampOption', *,
length=self._mh_option_length(schema.length),
timestamp=datetime.datetime.fromtimestamp(
seconds + fraction / 65536, tz=datetime.timezone.utc),
pmip_timestamp=PMIPv6Timestamp(seconds, fraction),
pmip_timestamp=Data_PMIPv6Timestamp(seconds=seconds, fraction=fraction),
)
return data

Expand Down Expand Up @@ -8382,7 +8370,7 @@ def _make_opt_auth(self, type: 'Enum_Option', option: 'Optional[Data_AuthOption]
)

def _make_opt_mesg_id(self, type: 'Enum_Option', option: 'Optional[Data_MesgIDOption]' = None, *,
timestamp: 'Optional[NTPTimestamp]' = None,
timestamp: 'Optional[Data_NTPTimestamp]' = None,
interval: 'Optional[dt_type]' = None,
**kwargs: 'Any') -> 'Schema_MesgIDOption':
"""Make MH mobility message replay protection option.
Expand Down Expand Up @@ -8410,8 +8398,8 @@ def _make_opt_mesg_id(self, type: 'Enum_Option', option: 'Optional[Data_MesgIDOp
ts_sec = math.floor(int_ts)
ts_frc = min(round((int_ts - ts_sec) * 2**32), 0xFFFF_FFFF) # 1/2**32 seconds per unit

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

return Schema_MesgIDOption(
type=type,
Expand Down Expand Up @@ -8982,7 +8970,8 @@ def _make_opt_timestamp(self, type: 'Enum_Option', option: 'Optional[Data_Timest

"""
if option is not None:
seconds, fraction = option.pmip_timestamp
seconds = option.pmip_timestamp.seconds
fraction = option.pmip_timestamp.fraction

if not 0 <= seconds < 2 ** 48:
raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid timestamp seconds: {seconds}')
Expand Down
Loading
Loading