From ddf28f9fb4806dbfae2668a5adc05a3a923dae0f Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Mon, 2 Nov 2026 15:57:52 -0500 Subject: [PATCH] feat(link): split radiotap's mixed bitmaps into value subfields (#2647) - Split the MCS flags, HE data1 and data2, HE-MU flags1 and flags2, and U-SIG common into their value subfields, each an int, and the rest of their bits. - Keep the rest as a member of a known-bits registry, so undefined bits still round-trip: HEData1Known, HEData2Known, HEMUFlags1Known, HEMUFlags2Known and USIGCommonKnown, crawled from radiotap.org, named after Linux's macros. - Add corekit's BitmapField, a word split from its least-significant bit in its byte order, and VALUES to the shared flag crawler for value rows. - make() takes each bitmap as a mapping of range-checked subfields, its known bits a member or an int; new data models for each word. - Tests: layouts against Linux, Wireshark and radiotap.org, both round trips, the crawlers and BitmapField; update the #2576 and #2655 expectations. The link, const, vendor, dumpkit, corekit, foundation, project and root protocols legs pass. --- docs/source/pcapkit/const/radiotap.rst | 75 ++++++ docs/source/pcapkit/corekit/fields/index.rst | 2 + .../source/pcapkit/corekit/fields/numbers.rst | 7 + .../pcapkit/protocols/link/radiotap.rst | 30 ++- docs/source/pcapkit/vendor/radiotap.rst | 74 +++++- .../generators/option_families/radiotap.py | 17 +- pcapkit/all.py | 1 + pcapkit/const/__init__.py | 6 +- pcapkit/const/radiotap/__init__.py | 21 +- pcapkit/const/radiotap/he_data1_known.py | 103 ++++++++ pcapkit/const/radiotap/he_data2_known.py | 88 +++++++ pcapkit/const/radiotap/he_mu_flags1_known.py | 85 +++++++ pcapkit/const/radiotap/he_mu_flags2_known.py | 67 +++++ pcapkit/const/radiotap/usig_common_known.py | 85 +++++++ pcapkit/corekit/__init__.py | 1 + pcapkit/corekit/fields/__init__.py | 7 +- pcapkit/corekit/fields/numbers.py | 136 +++++++++- pcapkit/protocols/data/__init__.py | 7 +- pcapkit/protocols/data/link/__init__.py | 13 +- pcapkit/protocols/data/link/radiotap.py | 185 ++++++++++++-- pcapkit/protocols/link/radiotap.py | 77 +++++- pcapkit/protocols/schema/link/radiotap.py | 77 ++++-- pcapkit/vendor/__init__.py | 6 +- pcapkit/vendor/radiotap/__init__.py | 21 +- pcapkit/vendor/radiotap/flags.py | 10 +- pcapkit/vendor/radiotap/he_data1_known.py | 54 ++++ pcapkit/vendor/radiotap/he_data2_known.py | 49 ++++ pcapkit/vendor/radiotap/he_mu_flags1_known.py | 48 ++++ pcapkit/vendor/radiotap/he_mu_flags2_known.py | 42 ++++ pcapkit/vendor/radiotap/usig_common_known.py | 48 ++++ tests/const/census/radiotap.py | 5 + tests/const/test_const_registry_protocol.py | 5 + tests/corekit/test_field_roundtrip_unit.py | 18 ++ .../test_fields_numbers_bitmap_2647_unit.py | 117 +++++++++ tests/corekit/test_schema_roundtrip_unit.py | 12 +- .../test_nameless_enum_rendering_unit.py | 3 + .../test_rfc_constants_one_home_1739_unit.py | 6 + .../link/test_radiotap_fields_2576_unit.py | 36 ++- .../test_radiotap_mixed_bitmaps_2647_unit.py | 191 ++++++++++++++ .../link/test_radiotap_short_tlv_2655_unit.py | 13 +- .../vendor/test_radiotap_crawler_2647_unit.py | 235 ++++++++++++++++++ 41 files changed, 1991 insertions(+), 92 deletions(-) create mode 100644 pcapkit/const/radiotap/he_data1_known.py create mode 100644 pcapkit/const/radiotap/he_data2_known.py create mode 100644 pcapkit/const/radiotap/he_mu_flags1_known.py create mode 100644 pcapkit/const/radiotap/he_mu_flags2_known.py create mode 100644 pcapkit/const/radiotap/usig_common_known.py create mode 100644 pcapkit/vendor/radiotap/he_data1_known.py create mode 100644 pcapkit/vendor/radiotap/he_data2_known.py create mode 100644 pcapkit/vendor/radiotap/he_mu_flags1_known.py create mode 100644 pcapkit/vendor/radiotap/he_mu_flags2_known.py create mode 100644 pcapkit/vendor/radiotap/usig_common_known.py create mode 100644 tests/corekit/test_fields_numbers_bitmap_2647_unit.py create mode 100644 tests/protocols/link/test_radiotap_mixed_bitmaps_2647_unit.py create mode 100644 tests/vendor/test_radiotap_crawler_2647_unit.py diff --git a/docs/source/pcapkit/const/radiotap.rst b/docs/source/pcapkit/const/radiotap.rst index 81020318f4..a47a1c9c97 100644 --- a/docs/source/pcapkit/const/radiotap.rst +++ b/docs/source/pcapkit/const/radiotap.rst @@ -32,12 +32,22 @@ from radiotap.org [*]_. Available enumerations include: - VHT Coding Flags * - :class:`Radiotap_TimestampFlags ` - Timestamp Flags + * - :class:`Radiotap_HEData1Known ` + - HE Data1 Known Flags + * - :class:`Radiotap_HEData2Known ` + - HE Data2 Known Flags + * - :class:`Radiotap_HEMUFlags1Known ` + - HE-MU Flags1 Known Flags + * - :class:`Radiotap_HEMUFlags2Known ` + - HE-MU Flags2 Known Flags * - :class:`Radiotap_HEMUOtherUserKnown ` - HE-MU-other-user Known Flags * - :class:`Radiotap_LSIGData1 ` - L-SIG Data1 Flags * - :class:`Radiotap_S1GKnown ` - S1G Known Flags + * - :class:`Radiotap_USIGCommonKnown ` + - U-SIG Common Known Flags * - :class:`Radiotap_EHTKnown ` - EHT Known Flags @@ -186,6 +196,58 @@ which is automatically generated from :class:`pcapkit.vendor.radiotap.timestamp_ :undoc-members: :show-inheritance: +HE Data1 Known Flags +==================== + +.. module:: pcapkit.const.radiotap.he_data1_known + +This module contains the constant enumeration for **Radiotap HE Data1 Known Flags**, +which is automatically generated from :class:`pcapkit.vendor.radiotap.he_data1_known.HEData1Known`. + +.. autoclass:: pcapkit.const.radiotap.he_data1_known.HEData1Known + :members: + :undoc-members: + :show-inheritance: + +HE Data2 Known Flags +==================== + +.. module:: pcapkit.const.radiotap.he_data2_known + +This module contains the constant enumeration for **Radiotap HE Data2 Known Flags**, +which is automatically generated from :class:`pcapkit.vendor.radiotap.he_data2_known.HEData2Known`. + +.. autoclass:: pcapkit.const.radiotap.he_data2_known.HEData2Known + :members: + :undoc-members: + :show-inheritance: + +HE-MU Flags1 Known Flags +======================== + +.. module:: pcapkit.const.radiotap.he_mu_flags1_known + +This module contains the constant enumeration for **Radiotap HE-MU Flags1 Known Flags**, +which is automatically generated from :class:`pcapkit.vendor.radiotap.he_mu_flags1_known.HEMUFlags1Known`. + +.. autoclass:: pcapkit.const.radiotap.he_mu_flags1_known.HEMUFlags1Known + :members: + :undoc-members: + :show-inheritance: + +HE-MU Flags2 Known Flags +======================== + +.. module:: pcapkit.const.radiotap.he_mu_flags2_known + +This module contains the constant enumeration for **Radiotap HE-MU Flags2 Known Flags**, +which is automatically generated from :class:`pcapkit.vendor.radiotap.he_mu_flags2_known.HEMUFlags2Known`. + +.. autoclass:: pcapkit.const.radiotap.he_mu_flags2_known.HEMUFlags2Known + :members: + :undoc-members: + :show-inheritance: + HE-MU-other-user Known Flags ============================ @@ -225,6 +287,19 @@ which is automatically generated from :class:`pcapkit.vendor.radiotap.s1g_known. :undoc-members: :show-inheritance: +U-SIG Common Known Flags +======================== + +.. module:: pcapkit.const.radiotap.usig_common_known + +This module contains the constant enumeration for **Radiotap U-SIG Common Known Flags**, +which is automatically generated from :class:`pcapkit.vendor.radiotap.usig_common_known.USIGCommonKnown`. + +.. autoclass:: pcapkit.const.radiotap.usig_common_known.USIGCommonKnown + :members: + :undoc-members: + :show-inheritance: + EHT Known Flags =============== diff --git a/docs/source/pcapkit/corekit/fields/index.rst b/docs/source/pcapkit/corekit/fields/index.rst index e5e0a0e78e..426a13d603 100644 --- a/docs/source/pcapkit/corekit/fields/index.rst +++ b/docs/source/pcapkit/corekit/fields/index.rst @@ -36,6 +36,7 @@ hierarchy: NumberField --> Int8Field & UInt8Field NumberField --> EnumField EnumField --> PortEnumField + NumberField --> BitmapField end C --> NumberField @@ -90,6 +91,7 @@ hierarchy: click UInt8Field "numbers.html#pcapkit.corekit.fields.numbers.UInt8Field" click EnumField "numbers.html#pcapkit.corekit.fields.numbers.EnumField" click PortEnumField "numbers.html#pcapkit.corekit.fields.numbers.PortEnumField" + click BitmapField "numbers.html#pcapkit.corekit.fields.numbers.BitmapField" click _TextField "strings.html#pcapkit.corekit.fields.strings._TextField" click BytesField "strings.html#pcapkit.corekit.fields.strings.BytesField" diff --git a/docs/source/pcapkit/corekit/fields/numbers.rst b/docs/source/pcapkit/corekit/fields/numbers.rst index 36f16147aa..e1e4cdea56 100644 --- a/docs/source/pcapkit/corekit/fields/numbers.rst +++ b/docs/source/pcapkit/corekit/fields/numbers.rst @@ -49,6 +49,13 @@ Enumeration Fields :members: :show-inheritance: +Bitmap Fields +------------- + +.. autoclass:: pcapkit.corekit.fields.numbers.BitmapField + :members: + :show-inheritance: + Internal Definitions -------------------- diff --git a/docs/source/pcapkit/protocols/link/radiotap.rst b/docs/source/pcapkit/protocols/link/radiotap.rst index a98e9ac18d..25592f13b6 100644 --- a/docs/source/pcapkit/protocols/link/radiotap.rst +++ b/docs/source/pcapkit/protocols/link/radiotap.rst @@ -72,7 +72,11 @@ retries, bits 16 and 17, from its unofficially used ones, which those three size alike; OpenBSD writes a 2-octet RSSI field at bit 16 instead, which is not read. A bitmap radiotap.org defines bit by bit is a member of its flag registry under :mod:`pcapkit.const.radiotap`, which keeps the bits it does not name. A -field type registered through +bitmap that mixes value subfields with known bits -- the MCS ``flags``, the HE +``data1`` and ``data2``, the HE-MU ``flags1`` and ``flags2`` and the U-SIG +``common`` -- is split: each value subfield is an integer, and its other bits, +the undefined ones among them, a member of its known-bits registry. A field +type registered through :func:`~pcapkit.foundation.registry.protocols.link.register_radiotap_field` is decoded the same way. @@ -445,6 +449,10 @@ Data Models :members: :show-inheritance: +.. autoclass:: pcapkit.protocols.data.link.radiotap.MCSFlags + :members: + :show-inheritance: + .. autoclass:: pcapkit.protocols.data.link.radiotap.AMPDUStatus :members: :show-inheritance: @@ -461,10 +469,26 @@ Data Models :members: :show-inheritance: +.. autoclass:: pcapkit.protocols.data.link.radiotap.HEData1 + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.link.radiotap.HEData2 + :members: + :show-inheritance: + .. autoclass:: pcapkit.protocols.data.link.radiotap.HEMU :members: :show-inheritance: +.. autoclass:: pcapkit.protocols.data.link.radiotap.HEMUFlags1 + :members: + :show-inheritance: + +.. autoclass:: pcapkit.protocols.data.link.radiotap.HEMUFlags2 + :members: + :show-inheritance: + .. autoclass:: pcapkit.protocols.data.link.radiotap.HEMUOtherUser :members: :show-inheritance: @@ -497,6 +521,10 @@ Data Models :members: :show-inheritance: +.. autoclass:: pcapkit.protocols.data.link.radiotap.USIGCommon + :members: + :show-inheritance: + .. autoclass:: pcapkit.protocols.data.link.radiotap.EHT :members: :show-inheritance: diff --git a/docs/source/pcapkit/vendor/radiotap.rst b/docs/source/pcapkit/vendor/radiotap.rst index 688c0420e3..82d0e09841 100644 --- a/docs/source/pcapkit/vendor/radiotap.rst +++ b/docs/source/pcapkit/vendor/radiotap.rst @@ -32,12 +32,22 @@ radiotap.org [*]_. Available vendor crawlers include: - VHT Coding Flags * - :class:`Radiotap_TimestampFlags ` - Timestamp Flags + * - :class:`Radiotap_HEData1Known ` + - HE Data1 Known Flags + * - :class:`Radiotap_HEData2Known ` + - HE Data2 Known Flags + * - :class:`Radiotap_HEMUFlags1Known ` + - HE-MU Flags1 Known Flags + * - :class:`Radiotap_HEMUFlags2Known ` + - HE-MU Flags2 Known Flags * - :class:`Radiotap_HEMUOtherUserKnown ` - HE-MU-other-user Known Flags * - :class:`Radiotap_LSIGData1 ` - L-SIG Data1 Flags * - :class:`Radiotap_S1GKnown ` - S1G Known Flags + * - :class:`Radiotap_USIGCommonKnown ` + - U-SIG Common Known Flags * - :class:`Radiotap_EHTKnown ` - EHT Known Flags @@ -78,7 +88,9 @@ which is automatically generating :class:`pcapkit.const.radiotap.flags.Flags`. Each flag crawler scrapes the table on its field's page, and subclasses this one. A row whose mask sets one bit is a member; a row that sets more bits, or that -radiotap.org marks reserved or unused, is not, and its bits stay undefined. A +radiotap.org marks reserved or unused, is not, and its bits stay undefined; nor +is a row of a crawler's ``VALUES``, a value subfield of a bitmap that mixes them +with flags, which its schema splits off. A member's name is the macro the crawler's ``NAMES`` gives its bit, less the ``IEEE80211_RADIOTAP_`` or ``IEEE80211_`` prefix: the Linux kernel's, from ``include/net/ieee80211_radiotap.h``, or Wireshark's where Linux names none. @@ -196,6 +208,54 @@ which is automatically generating :class:`pcapkit.const.radiotap.timestamp_flags :members: FLAG, LINK, SUBFIELD, NAMES :show-inheritance: +HE Data1 Known Flags +==================== + +.. module:: pcapkit.vendor.radiotap.he_data1_known + +This module contains the vendor crawler for **Radiotap HE Data1 Known Flags**, +which is automatically generating :class:`pcapkit.const.radiotap.he_data1_known.HEData1Known`. + +.. autoclass:: pcapkit.vendor.radiotap.he_data1_known.HEData1Known + :members: FLAG, LINK, SUBFIELD, VALUES, NAMES + :show-inheritance: + +HE Data2 Known Flags +==================== + +.. module:: pcapkit.vendor.radiotap.he_data2_known + +This module contains the vendor crawler for **Radiotap HE Data2 Known Flags**, +which is automatically generating :class:`pcapkit.const.radiotap.he_data2_known.HEData2Known`. + +.. autoclass:: pcapkit.vendor.radiotap.he_data2_known.HEData2Known + :members: FLAG, LINK, SUBFIELD, VALUES, NAMES + :show-inheritance: + +HE-MU Flags1 Known Flags +======================== + +.. module:: pcapkit.vendor.radiotap.he_mu_flags1_known + +This module contains the vendor crawler for **Radiotap HE-MU Flags1 Known Flags**, +which is automatically generating :class:`pcapkit.const.radiotap.he_mu_flags1_known.HEMUFlags1Known`. + +.. autoclass:: pcapkit.vendor.radiotap.he_mu_flags1_known.HEMUFlags1Known + :members: FLAG, LINK, SUBFIELD, VALUES, NAMES + :show-inheritance: + +HE-MU Flags2 Known Flags +======================== + +.. module:: pcapkit.vendor.radiotap.he_mu_flags2_known + +This module contains the vendor crawler for **Radiotap HE-MU Flags2 Known Flags**, +which is automatically generating :class:`pcapkit.const.radiotap.he_mu_flags2_known.HEMUFlags2Known`. + +.. autoclass:: pcapkit.vendor.radiotap.he_mu_flags2_known.HEMUFlags2Known + :members: FLAG, LINK, SUBFIELD, VALUES, NAMES + :show-inheritance: + HE-MU-other-user Known Flags ============================ @@ -232,6 +292,18 @@ which is automatically generating :class:`pcapkit.const.radiotap.s1g_known.S1GKn :members: FLAG, LINK, SUBFIELD, NAMES :show-inheritance: +U-SIG Common Known Flags +======================== + +.. module:: pcapkit.vendor.radiotap.usig_common_known + +This module contains the vendor crawler for **Radiotap U-SIG Common Known Flags**, +which is automatically generating :class:`pcapkit.const.radiotap.usig_common_known.USIGCommonKnown`. + +.. autoclass:: pcapkit.vendor.radiotap.usig_common_known.USIGCommonKnown + :members: FLAG, LINK, SUBFIELD, VALUES, NAMES + :show-inheritance: + EHT Known Flags =============== diff --git a/examples/generators/option_families/radiotap.py b/examples/generators/option_families/radiotap.py index a5187f3753..277158301e 100644 --- a/examples/generators/option_families/radiotap.py +++ b/examples/generators/option_families/radiotap.py @@ -54,19 +54,28 @@ def _radiotap_field_overrides() -> 'dict[Any, dict[str, Any]]': Enum_FieldType.TX_flags: {'flags': 0x0008}, Enum_FieldType.RTS_retries: {'retries': 3}, Enum_FieldType.data_retries: {'retries': 1}, - Enum_FieldType.MCS: {'known': 0x27, 'flags': 0x25, 'mcs': 7}, + Enum_FieldType.MCS: {'known': 0x27, 'flags': {'bandwidth': 1, 'guard_interval': 1, 'ht_format': 0, + 'fec_type': 0, 'stbc_streams': 1, 'ness_bit0': 0}, + 'mcs': 7}, Enum_FieldType.A_MPDU_status: {'reference': 7, 'flags': 0x0105, 'delimiter_crc': 0xAB, 'reserved': 0}, Enum_FieldType.VHT: {'known': 0x0244, 'flags': 0x05, 'bandwidth': 4, 'mcs_nss': (0x91, 0, 0, 0), 'coding': 0x01, 'group_id': 2, 'partial_aid': 0x1234}, Enum_FieldType.timestamp: {'timestamp': 1, 'accuracy': 2, 'unit_position': 0x11, 'flags': 0x02}, - Enum_FieldType.HE: {'data1': 1, 'data2': 2, 'data3': 3, 'data4': 4, 'data5': 5, 'data6': 6}, - Enum_FieldType.HE_MU: {'flags1': 1, 'flags2': 2, 'ru_channel1': (1, 2, 3, 4), 'ru_channel2': (5, 6, 7, 8)}, + Enum_FieldType.HE: {'data1': {'ppdu_format': 1, 'known': 0x0004}, + 'data2': {'ru_offset': 5, 'pri_sec_80mhz': 1, 'known': 0x4001}, + 'data3': 3, 'data4': 4, 'data5': 5, 'data6': 6}, + Enum_FieldType.HE_MU: {'flags1': {'sig_b_mcs': 3, 'sig_b_dcm': 1, 'ch1_center_26_tone_ru': 1, 'known': 0x0010}, + 'flags2': {'bandwidth': 2, 'sig_b_compression': 1, 'sig_b_symbols_users': 4, + 'preamble_puncturing': 1, 'ch2_center_26_tone_ru': 1, 'known': 0x0404}, + 'ru_channel1': (1, 2, 3, 4), 'ru_channel2': (5, 6, 7, 8)}, Enum_FieldType.HE_MU_other_user: {'per_user_1': 1, 'per_user_2': 2, 'per_user_position': 3, 'per_user_known': 0x23}, Enum_FieldType.FieldType_0_length_PSDU: {'psdu_type': 1}, Enum_FieldType.L_SIG: {'data1': 0x0007, 'data2': 0x1234}, Enum_FieldType.S1G: {'known': 0x0103, 'data1': 2, 'data2': 3}, - Enum_FieldType.U_SIG: {'common': 0x11, 'value': 0x22, 'mask': 0x33}, + Enum_FieldType.U_SIG: {'common': {'phy_version': 0, 'bandwidth': 4, 'ul_dl': 1, 'bss_color': 9, 'txop': 3, + 'known': 0x1F}, + 'value': 0x22, 'mask': 0x33}, Enum_FieldType.EHT: {'known': 3, 'data': tuple(range(9)), 'user_info': (0xA1, 0xA2)}, } diff --git a/pcapkit/all.py b/pcapkit/all.py index 653d48c8c8..c860de3766 100644 --- a/pcapkit/all.py +++ b/pcapkit/all.py @@ -59,6 +59,7 @@ 'NumberField', 'Int32Field', 'UInt32Field', # numeric protocol fields 'Int16Field', 'UInt16Field', 'Int64Field', 'UInt64Field', 'Int8Field', 'UInt8Field', 'EnumField', 'PortEnumField', + 'BitmapField', 'BytesField', 'StringField', 'BitField', # text protocol fields 'PaddingField', 'ConditionalField', 'PayloadField', 'SchemaField', # misc protocol fields diff --git a/pcapkit/const/__init__.py b/pcapkit/const/__init__.py index 2ef92b6b5c..bfe312953e 100644 --- a/pcapkit/const/__init__.py +++ b/pcapkit/const/__init__.py @@ -104,8 +104,10 @@ # Radiotap 'Radiotap_FieldType', 'Radiotap_Flags', 'Radiotap_ChannelFlags', 'Radiotap_RXFlags', 'Radiotap_TXFlags', 'Radiotap_MCSKnown', 'Radiotap_AMPDUFlags', 'Radiotap_VHTKnown', - 'Radiotap_VHTFlags', 'Radiotap_VHTCoding', 'Radiotap_TimestampFlags', - 'Radiotap_HEMUOtherUserKnown', 'Radiotap_LSIGData1', 'Radiotap_S1GKnown', 'Radiotap_EHTKnown', + 'Radiotap_VHTFlags', 'Radiotap_VHTCoding', 'Radiotap_TimestampFlags', 'Radiotap_HEData1Known', + 'Radiotap_HEData2Known', 'Radiotap_HEMUFlags1Known', 'Radiotap_HEMUFlags2Known', + 'Radiotap_HEMUOtherUserKnown', 'Radiotap_LSIGData1', 'Radiotap_S1GKnown', + 'Radiotap_USIGCommonKnown', 'Radiotap_EHTKnown', # SCTP 'SCTP_Chunk', 'SCTP_Parameter', 'SCTP_CauseCode', 'SCTP_PayloadProtocolIdentifier', # SSLv2 diff --git a/pcapkit/const/radiotap/__init__.py b/pcapkit/const/radiotap/__init__.py index 62bd506eb7..453d4d66f3 100644 --- a/pcapkit/const/radiotap/__init__.py +++ b/pcapkit/const/radiotap/__init__.py @@ -33,12 +33,22 @@ - VHT Coding Flags * - :class:`Radiotap_TimestampFlags ` - Timestamp Flags + * - :class:`Radiotap_HEData1Known ` + - HE Data1 Known Flags + * - :class:`Radiotap_HEData2Known ` + - HE Data2 Known Flags + * - :class:`Radiotap_HEMUFlags1Known ` + - HE-MU Flags1 Known Flags + * - :class:`Radiotap_HEMUFlags2Known ` + - HE-MU Flags2 Known Flags * - :class:`Radiotap_HEMUOtherUserKnown ` - HE-MU-other-user Known Flags * - :class:`Radiotap_LSIGData1 ` - L-SIG Data1 Flags * - :class:`Radiotap_S1GKnown ` - S1G Known Flags + * - :class:`Radiotap_USIGCommonKnown ` + - U-SIG Common Known Flags * - :class:`Radiotap_EHTKnown ` - EHT Known Flags @@ -49,6 +59,10 @@ from pcapkit.const.radiotap.eht_known import EHTKnown as Radiotap_EHTKnown from pcapkit.const.radiotap.field_type import FieldType as Radiotap_FieldType from pcapkit.const.radiotap.flags import Flags as Radiotap_Flags +from pcapkit.const.radiotap.he_data1_known import HEData1Known as Radiotap_HEData1Known +from pcapkit.const.radiotap.he_data2_known import HEData2Known as Radiotap_HEData2Known +from pcapkit.const.radiotap.he_mu_flags1_known import HEMUFlags1Known as Radiotap_HEMUFlags1Known +from pcapkit.const.radiotap.he_mu_flags2_known import HEMUFlags2Known as Radiotap_HEMUFlags2Known from pcapkit.const.radiotap.he_mu_other_user_known import \ HEMUOtherUserKnown as Radiotap_HEMUOtherUserKnown from pcapkit.const.radiotap.lsig_data1 import LSIGData1 as Radiotap_LSIGData1 @@ -57,6 +71,7 @@ from pcapkit.const.radiotap.s1g_known import S1GKnown as Radiotap_S1GKnown from pcapkit.const.radiotap.timestamp_flags import TimestampFlags as Radiotap_TimestampFlags from pcapkit.const.radiotap.tx_flags import TXFlags as Radiotap_TXFlags +from pcapkit.const.radiotap.usig_common_known import USIGCommonKnown as Radiotap_USIGCommonKnown from pcapkit.const.radiotap.vht_coding import VHTCoding as Radiotap_VHTCoding from pcapkit.const.radiotap.vht_flags import VHTFlags as Radiotap_VHTFlags from pcapkit.const.radiotap.vht_known import VHTKnown as Radiotap_VHTKnown @@ -64,6 +79,8 @@ __all__ = [ 'Radiotap_FieldType', 'Radiotap_Flags', 'Radiotap_ChannelFlags', 'Radiotap_RXFlags', 'Radiotap_TXFlags', 'Radiotap_MCSKnown', 'Radiotap_AMPDUFlags', 'Radiotap_VHTKnown', - 'Radiotap_VHTFlags', 'Radiotap_VHTCoding', 'Radiotap_TimestampFlags', - 'Radiotap_HEMUOtherUserKnown', 'Radiotap_LSIGData1', 'Radiotap_S1GKnown', 'Radiotap_EHTKnown', + 'Radiotap_VHTFlags', 'Radiotap_VHTCoding', 'Radiotap_TimestampFlags', 'Radiotap_HEData1Known', + 'Radiotap_HEData2Known', 'Radiotap_HEMUFlags1Known', 'Radiotap_HEMUFlags2Known', + 'Radiotap_HEMUOtherUserKnown', 'Radiotap_LSIGData1', 'Radiotap_S1GKnown', + 'Radiotap_USIGCommonKnown', 'Radiotap_EHTKnown', ] diff --git a/pcapkit/const/radiotap/he_data1_known.py b/pcapkit/const/radiotap/he_data1_known.py new file mode 100644 index 0000000000..9227b9e1c4 --- /dev/null +++ b/pcapkit/const/radiotap/he_data1_known.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,consider-using-f-string +"""Radiotap HE Data1 Known Flags +=================================== + +.. module:: pcapkit.const.radiotap.he_data1_known + +This module contains the constant enumeration for **Radiotap HE Data1 Known Flags**, +which is automatically generated from :class:`pcapkit.vendor.radiotap.he_data1_known.HEData1Known`. + +""" + +from typing import TYPE_CHECKING + +from aenum import IntFlag + +from pcapkit.corekit.enum import EnumRegistry +from pcapkit.utilities.exceptions import EnumValueError + +if TYPE_CHECKING: + from typing import Any + +__all__ = ['HEData1Known'] + + +class HEData1Known(EnumRegistry, IntFlag): + """[HEData1Known] Radiotap HE Data1 Known Flags""" + + #: BSS Color known + HE_DATA1_BSS_COLOR_KNOWN = 1 << 2 + + #: Beam Change known + HE_DATA1_BEAM_CHANGE_KNOWN = 1 << 3 + + #: UL/DL known + HE_DATA1_UL_DL_KNOWN = 1 << 4 + + #: data MCS known + HE_DATA1_DATA_MCS_KNOWN = 1 << 5 + + #: data DCM known + HE_DATA1_DATA_DCM_KNOWN = 1 << 6 + + #: Coding known + HE_DATA1_CODING_KNOWN = 1 << 7 + + #: LDPC extra symbol segment known + HE_DATA1_LDPC_XSYMSEG_KNOWN = 1 << 8 + + #: STBC known + HE_DATA1_STBC_KNOWN = 1 << 9 + + #: Spatial Reuse known (Spatial Reuse 1 for HE_TRIG format) + HE_DATA1_SPTL_REUSE_KNOWN = 1 << 10 + + #: Spatial Reuse 2 known (HE_TRIG format), STA-ID known (HE_MU format) + HE_DATA1_SPTL_REUSE2_KNOWN = 1 << 11 + + #: Spatial Reuse 3 known (HE_TRIG format) + HE_DATA1_SPTL_REUSE3_KNOWN = 1 << 12 + + #: Spatial Reuse 4 known (HE_TRIG format) + HE_DATA1_SPTL_REUSE4_KNOWN = 1 << 13 + + #: data BW/RU allocation known + HE_DATA1_BW_RU_ALLOC_KNOWN = 1 << 14 + + #: Doppler known + HE_DATA1_DOPPLER_KNOWN = 1 << 15 + + @classmethod + def _validate_value(cls, value: 'Any') -> 'None': + """Reject ``value`` unless it passes the range check :meth:`_missing_` makes. + + Overrides :meth:`~pcapkit.corekit.enum.EnumLookup._validate_value` + with that check and the message it raises, so + :meth:`~pcapkit.corekit.enum.EnumRegistry.register` refuses exactly the + values :meth:`~pcapkit.corekit.enum.EnumLookup.get` refuses, in the + same words (:issue:`1816`). + + Args: + value: Value about to be looked up or minted. + + Raises: + EnumValueError: If ``value`` fails that check. A + :exc:`ValueError`, so an ``except ValueError`` caller is + unaffected. + + """ + if not (isinstance(value, int) and 0 <= value <= 0xFFFF): + raise EnumValueError('%r is not a valid %s' % (value, cls.__name__)) + + @classmethod + def _missing_(cls, value: 'int') -> 'HEData1Known': + """Lookup function used when value is not found. + + Args: + value: Value to get enum item. + + """ + if not (isinstance(value, int) and 0 <= value <= 0xFFFF): + raise ValueError('%r is not a valid %s' % (value, cls.__name__)) + return super()._missing_(value) diff --git a/pcapkit/const/radiotap/he_data2_known.py b/pcapkit/const/radiotap/he_data2_known.py new file mode 100644 index 0000000000..4e6a2df856 --- /dev/null +++ b/pcapkit/const/radiotap/he_data2_known.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,consider-using-f-string +"""Radiotap HE Data2 Known Flags +=================================== + +.. module:: pcapkit.const.radiotap.he_data2_known + +This module contains the constant enumeration for **Radiotap HE Data2 Known Flags**, +which is automatically generated from :class:`pcapkit.vendor.radiotap.he_data2_known.HEData2Known`. + +""" + +from typing import TYPE_CHECKING + +from aenum import IntFlag + +from pcapkit.corekit.enum import EnumRegistry +from pcapkit.utilities.exceptions import EnumValueError + +if TYPE_CHECKING: + from typing import Any + +__all__ = ['HEData2Known'] + + +class HEData2Known(EnumRegistry, IntFlag): + """[HEData2Known] Radiotap HE Data2 Known Flags""" + + #: pri/sec 80 MHz known + HE_DATA2_PRISEC_80_KNOWN = 1 << 0 + + #: GI known + HE_DATA2_GI_KNOWN = 1 << 1 + + #: number of LTF symbols known + HE_DATA2_NUM_LTF_SYMS_KNOWN = 1 << 2 + + #: Pre-FEC Padding Factor known + HE_DATA2_PRE_FEC_PAD_KNOWN = 1 << 3 + + #: TxBF known + HE_DATA2_TXBF_KNOWN = 1 << 4 + + #: PE Disambiguity known + HE_DATA2_PE_DISAMBIG_KNOWN = 1 << 5 + + #: TXOP known + HE_DATA2_TXOP_KNOWN = 1 << 6 + + #: midamble periodicity known + HE_DATA2_MIDAMBLE_KNOWN = 1 << 7 + + #: RU allocation offset known + HE_DATA2_RU_OFFSET_KNOWN = 1 << 14 + + @classmethod + def _validate_value(cls, value: 'Any') -> 'None': + """Reject ``value`` unless it passes the range check :meth:`_missing_` makes. + + Overrides :meth:`~pcapkit.corekit.enum.EnumLookup._validate_value` + with that check and the message it raises, so + :meth:`~pcapkit.corekit.enum.EnumRegistry.register` refuses exactly the + values :meth:`~pcapkit.corekit.enum.EnumLookup.get` refuses, in the + same words (:issue:`1816`). + + Args: + value: Value about to be looked up or minted. + + Raises: + EnumValueError: If ``value`` fails that check. A + :exc:`ValueError`, so an ``except ValueError`` caller is + unaffected. + + """ + if not (isinstance(value, int) and 0 <= value <= 0xFFFF): + raise EnumValueError('%r is not a valid %s' % (value, cls.__name__)) + + @classmethod + def _missing_(cls, value: 'int') -> 'HEData2Known': + """Lookup function used when value is not found. + + Args: + value: Value to get enum item. + + """ + if not (isinstance(value, int) and 0 <= value <= 0xFFFF): + raise ValueError('%r is not a valid %s' % (value, cls.__name__)) + return super()._missing_(value) diff --git a/pcapkit/const/radiotap/he_mu_flags1_known.py b/pcapkit/const/radiotap/he_mu_flags1_known.py new file mode 100644 index 0000000000..c3ca8f4c01 --- /dev/null +++ b/pcapkit/const/radiotap/he_mu_flags1_known.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,consider-using-f-string +"""Radiotap HE-MU Flags1 Known Flags +======================================= + +.. module:: pcapkit.const.radiotap.he_mu_flags1_known + +This module contains the constant enumeration for **Radiotap HE-MU Flags1 Known Flags**, +which is automatically generated from :class:`pcapkit.vendor.radiotap.he_mu_flags1_known.HEMUFlags1Known`. + +""" + +from typing import TYPE_CHECKING + +from aenum import IntFlag + +from pcapkit.corekit.enum import EnumRegistry +from pcapkit.utilities.exceptions import EnumValueError + +if TYPE_CHECKING: + from typing import Any + +__all__ = ['HEMUFlags1Known'] + + +class HEMUFlags1Known(EnumRegistry, IntFlag): + """[HEMUFlags1Known] Radiotap HE-MU Flags1 Known Flags""" + + #: SIG-B MCS known + HE_MU_FLAGS1_SIG_B_MCS_KNOWN = 1 << 4 + + #: SIG-B DCM known + HE_MU_FLAGS1_SIG_B_DCM_KNOWN = 1 << 6 + + #: (Channel 2) Center 26-tone RU bit known + HE_MU_FLAGS1_CH2_CTR_26T_RU_KNOWN = 1 << 7 + + #: Channel 1 RUs known (which depends on BW) + HE_MU_FLAGS1_CH1_RU_KNOWN = 1 << 8 + + #: Channel 2 RUs known (which depends on BW) + HE_MU_FLAGS1_CH2_RU_KNOWN = 1 << 9 + + #: (Channel 1) Center 26-tone RU bit known + HE_MU_FLAGS1_CH1_CTR_26T_RU_KNOWN = 1 << 12 + + #: SIG-B Compression known + HE_MU_FLAGS1_SIG_B_COMP_KNOWN = 1 << 14 + + #: # of HE-SIG-B Symbols/MU-MIMO Users known + HE_MU_FLAGS1_SIG_B_SYMS_USERS_KNOWN = 1 << 15 + + @classmethod + def _validate_value(cls, value: 'Any') -> 'None': + """Reject ``value`` unless it passes the range check :meth:`_missing_` makes. + + Overrides :meth:`~pcapkit.corekit.enum.EnumLookup._validate_value` + with that check and the message it raises, so + :meth:`~pcapkit.corekit.enum.EnumRegistry.register` refuses exactly the + values :meth:`~pcapkit.corekit.enum.EnumLookup.get` refuses, in the + same words (:issue:`1816`). + + Args: + value: Value about to be looked up or minted. + + Raises: + EnumValueError: If ``value`` fails that check. A + :exc:`ValueError`, so an ``except ValueError`` caller is + unaffected. + + """ + if not (isinstance(value, int) and 0 <= value <= 0xFFFF): + raise EnumValueError('%r is not a valid %s' % (value, cls.__name__)) + + @classmethod + def _missing_(cls, value: 'int') -> 'HEMUFlags1Known': + """Lookup function used when value is not found. + + Args: + value: Value to get enum item. + + """ + if not (isinstance(value, int) and 0 <= value <= 0xFFFF): + raise ValueError('%r is not a valid %s' % (value, cls.__name__)) + return super()._missing_(value) diff --git a/pcapkit/const/radiotap/he_mu_flags2_known.py b/pcapkit/const/radiotap/he_mu_flags2_known.py new file mode 100644 index 0000000000..7240837ae9 --- /dev/null +++ b/pcapkit/const/radiotap/he_mu_flags2_known.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,consider-using-f-string +"""Radiotap HE-MU Flags2 Known Flags +======================================= + +.. module:: pcapkit.const.radiotap.he_mu_flags2_known + +This module contains the constant enumeration for **Radiotap HE-MU Flags2 Known Flags**, +which is automatically generated from :class:`pcapkit.vendor.radiotap.he_mu_flags2_known.HEMUFlags2Known`. + +""" + +from typing import TYPE_CHECKING + +from aenum import IntFlag + +from pcapkit.corekit.enum import EnumRegistry +from pcapkit.utilities.exceptions import EnumValueError + +if TYPE_CHECKING: + from typing import Any + +__all__ = ['HEMUFlags2Known'] + + +class HEMUFlags2Known(EnumRegistry, IntFlag): + """[HEMUFlags2Known] Radiotap HE-MU Flags2 Known Flags""" + + #: bandwidth from Bandwidth field in HE-SIG-A known + HE_MU_FLAGS2_BW_FROM_SIG_A_BW_KNOWN = 1 << 2 + + #: preamble puncturing from Bandwidth field in HE-SIG-A known + HE_MU_FLAGS2_PUNC_FROM_SIG_A_BW_KNOWN = 1 << 10 + + @classmethod + def _validate_value(cls, value: 'Any') -> 'None': + """Reject ``value`` unless it passes the range check :meth:`_missing_` makes. + + Overrides :meth:`~pcapkit.corekit.enum.EnumLookup._validate_value` + with that check and the message it raises, so + :meth:`~pcapkit.corekit.enum.EnumRegistry.register` refuses exactly the + values :meth:`~pcapkit.corekit.enum.EnumLookup.get` refuses, in the + same words (:issue:`1816`). + + Args: + value: Value about to be looked up or minted. + + Raises: + EnumValueError: If ``value`` fails that check. A + :exc:`ValueError`, so an ``except ValueError`` caller is + unaffected. + + """ + if not (isinstance(value, int) and 0 <= value <= 0xFFFF): + raise EnumValueError('%r is not a valid %s' % (value, cls.__name__)) + + @classmethod + def _missing_(cls, value: 'int') -> 'HEMUFlags2Known': + """Lookup function used when value is not found. + + Args: + value: Value to get enum item. + + """ + if not (isinstance(value, int) and 0 <= value <= 0xFFFF): + raise ValueError('%r is not a valid %s' % (value, cls.__name__)) + return super()._missing_(value) diff --git a/pcapkit/const/radiotap/usig_common_known.py b/pcapkit/const/radiotap/usig_common_known.py new file mode 100644 index 0000000000..6605923f45 --- /dev/null +++ b/pcapkit/const/radiotap/usig_common_known.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,consider-using-f-string +"""Radiotap U-SIG Common Known Flags +======================================= + +.. module:: pcapkit.const.radiotap.usig_common_known + +This module contains the constant enumeration for **Radiotap U-SIG Common Known Flags**, +which is automatically generated from :class:`pcapkit.vendor.radiotap.usig_common_known.USIGCommonKnown`. + +""" + +from typing import TYPE_CHECKING + +from aenum import IntFlag + +from pcapkit.corekit.enum import EnumRegistry +from pcapkit.utilities.exceptions import EnumValueError + +if TYPE_CHECKING: + from typing import Any + +__all__ = ['USIGCommonKnown'] + + +class USIGCommonKnown(EnumRegistry, IntFlag): + """[USIGCommonKnown] Radiotap U-SIG Common Known Flags""" + + #: PHY version identifier known + EHT_USIG_COMMON_PHY_VER_KNOWN = 1 << 0 + + #: BW known + EHT_USIG_COMMON_BW_KNOWN = 1 << 1 + + #: UL/DL known + EHT_USIG_COMMON_UL_DL_KNOWN = 1 << 2 + + #: BSS Color known + EHT_USIG_COMMON_BSS_COLOR_KNOWN = 1 << 3 + + #: TXOP known + EHT_USIG_COMMON_TXOP_KNOWN = 1 << 4 + + #: bad U-SIG CRC + EHT_USIG_COMMON_BAD_USIG_CRC = 1 << 5 + + #: validate bits checked + EHT_USIG_COMMON_VALIDATE_BITS_CHECKED = 1 << 6 + + #: validate bits OK + EHT_USIG_COMMON_VALIDATE_BITS_OK = 1 << 7 + + @classmethod + def _validate_value(cls, value: 'Any') -> 'None': + """Reject ``value`` unless it passes the range check :meth:`_missing_` makes. + + Overrides :meth:`~pcapkit.corekit.enum.EnumLookup._validate_value` + with that check and the message it raises, so + :meth:`~pcapkit.corekit.enum.EnumRegistry.register` refuses exactly the + values :meth:`~pcapkit.corekit.enum.EnumLookup.get` refuses, in the + same words (:issue:`1816`). + + Args: + value: Value about to be looked up or minted. + + Raises: + EnumValueError: If ``value`` fails that check. A + :exc:`ValueError`, so an ``except ValueError`` caller is + unaffected. + + """ + if not (isinstance(value, int) and 0 <= value <= 0xFFFFFFFF): + raise EnumValueError('%r is not a valid %s' % (value, cls.__name__)) + + @classmethod + def _missing_(cls, value: 'int') -> 'USIGCommonKnown': + """Lookup function used when value is not found. + + Args: + value: Value to get enum item. + + """ + if not (isinstance(value, int) and 0 <= value <= 0xFFFFFFFF): + raise ValueError('%r is not a valid %s' % (value, cls.__name__)) + return super()._missing_(value) diff --git a/pcapkit/corekit/__init__.py b/pcapkit/corekit/__init__.py index 7d3f380532..d26490e268 100644 --- a/pcapkit/corekit/__init__.py +++ b/pcapkit/corekit/__init__.py @@ -43,6 +43,7 @@ class :class:`~pcapkit.corekit.infoclass.Info`, 'Int64Field', 'UInt64Field', 'Int8Field', 'UInt8Field', 'EnumField', 'PortEnumField', + 'BitmapField', 'BytesField', 'StringField', diff --git a/pcapkit/corekit/fields/__init__.py b/pcapkit/corekit/fields/__init__.py index 6f4ab908bb..d8a35894d7 100644 --- a/pcapkit/corekit/fields/__init__.py +++ b/pcapkit/corekit/fields/__init__.py @@ -17,9 +17,9 @@ IPv6AddressField, IPv6InterfaceField) from pcapkit.corekit.fields.misc import (ConditionalField, ForwardMatchField, NoValueField, PayloadField, SchemaField, SwitchField) -from pcapkit.corekit.fields.numbers import (EnumField, Int8Field, Int16Field, Int32Field, - Int64Field, NumberField, PortEnumField, UInt8Field, - UInt16Field, UInt32Field, UInt64Field) +from pcapkit.corekit.fields.numbers import (BitmapField, EnumField, Int8Field, Int16Field, + Int32Field, Int64Field, NumberField, PortEnumField, + UInt8Field, UInt16Field, UInt32Field, UInt64Field) from pcapkit.corekit.fields.strings import BitField, BytesField, PaddingField, StringField __all__ = [ @@ -29,6 +29,7 @@ 'Int64Field', 'UInt64Field', 'Int8Field', 'UInt8Field', 'EnumField', 'PortEnumField', + 'BitmapField', 'BytesField', 'StringField', diff --git a/pcapkit/corekit/fields/numbers.py b/pcapkit/corekit/fields/numbers.py index 5001257f9a..643c34e7ef 100644 --- a/pcapkit/corekit/fields/numbers.py +++ b/pcapkit/corekit/fields/numbers.py @@ -19,11 +19,12 @@ 'Int64Field', 'UInt64Field', 'Int8Field', 'UInt8Field', 'EnumField', 'PortEnumField', + 'BitmapField', ] if TYPE_CHECKING: from enum import IntEnum as StdlibEnum - from typing import Any, Callable, Optional, Type + from typing import Any, Callable, Mapping, Optional, Type from aenum import IntEnum as AenumEnum from typing_extensions import Literal, Self @@ -1198,3 +1199,136 @@ def post_process(self, value: 'int | bytes', packet: 'dict[str, Any]') -> 'AppTy owner, f'unknown [{value:d} - {transport.name}]', svc='unknown', port=value, proto=transport) return self._namespace.get(value, proto=proto) + + +class BitmapField(NumberField): + """Integer word for protocol fields, split into subfields numbered from its + least-significant bit. + + The word is read as an integer in its byte order first, so + a subfield may cross an octet boundary of a little-endian word, which + :class:`~pcapkit.corekit.fields.strings.BitField`, numbering the bits of the + raw octets from the most-significant one, cannot express. + + Args: + length: Field size (in bytes). + namespace: Field namespace (a dict mapping each subfield's name to a tuple + of its start bit, counted from the least-significant bit, and its + width in bits). + remainder: Name the bits no subfield names are kept under, in place; with + :obj:`None`, they are kept under ``__unnamed__`` where any is set, as + :class:`~pcapkit.corekit.fields.strings.BitField` keeps them. + flags: Flag enumeration :attr:`remainder` holds a member of; with + :obj:`None`, it holds an :class:`int`. + byteorder: Field byte order. + callback: Callback function to be called upon + :meth:`self.__call__ `. + + Raises: + FieldValueError: If a subfield reaches past the word, two subfields share + a bit, or ``flags`` is given without ``remainder``. + + .. versionadded:: 1.6.0 + + """ + + #: Key of the unpacked value that carries the unnamed bits, as + #: :attr:`BitField.UNNAMED `. + UNNAMED = '__unnamed__' + + @property + def subfields(self) -> 'dict[str, tuple[int, int]]': + """Each subfield's start bit and width.""" + return dict(self._subfields) + + @property + def remainder(self) -> 'Optional[str]': + """Name the bits no subfield names are kept under.""" + return self._remainder + + @property + def remainder_mask(self) -> 'int': + """Mask of the bits no subfield names.""" + return self._remainder_mask + + @property + def flags(self) -> 'Optional[type]': + """Flag enumeration :attr:`remainder` holds a member of.""" + return self._registry + + def __init__(self, length: 'int', namespace: 'dict[str, tuple[int, int]]', *, + remainder: 'Optional[str]' = None, flags: 'Optional[Type[enum.Flag] | Type[aenum.Flag]]' = None, + byteorder: 'Literal["little", "big"]' = 'big', + callback: 'Callable[[Self, dict[str, Any]], None]' = lambda *_: None) -> 'None': + super().__init__(length, byteorder=byteorder, callback=callback) + + if flags is not None and remainder is None: + raise FieldValueError(f'{type(self).__name__}: flags {flags.__name__} given without a remainder') + width = 8 * length + named = 0 + for name, (start, size) in namespace.items(): + bits = ((1 << size) - 1) << start + if start < 0 or size < 1 or start + size > width or named & bits: + raise FieldValueError(f'{type(self).__name__}: subfield {name!r} spans bits ' + f'{start}-{start + size - 1} of a {width}-bit word, or another\'s') + named |= bits + + self._subfields = dict(namespace) + self._remainder = remainder + self._registry = flags + self._remainder_mask = ((1 << width) - 1) & ~named + + def pre_process(self, value: 'Mapping[str, Any]', # type: ignore[override] + packet: 'dict[str, Any]') -> 'int | bytes': + """Process field value before construction (packing). + + Arguments: + value: Field value. + packet: Packet data. + + Returns: + Processed field value. + + Raises: + FieldValueError: If a subfield is missing or its value does not fit + its bits, or the remainder is not an :class:`int` or sets a + subfield's bit. + + """ + word = 0 + for name, (start, size) in self._subfields.items(): + if name not in value: + raise FieldValueError(f'{type(self).__name__}: subfield {name!r} is missing') + part = value[name] + if isinstance(part, bool) or not isinstance(part, int) or not 0 <= part < 1 << size: + raise FieldValueError(f'{type(self).__name__}: subfield {name!r} value {part!r} ' + f'does not fit in {size} bit(s)') + word |= part << start + rest = value.get(self.UNNAMED if self._remainder is None else self._remainder, 0) + if isinstance(rest, bool) or not isinstance(rest, int) or int(rest) & ~self._remainder_mask: + raise FieldValueError(f'{type(self).__name__}: remainder {rest!r} is not an int of the bits ' + 'no subfield names') + return super().pre_process(word | int(rest), packet) + + def post_process(self, value: 'int | bytes', # type: ignore[override] + packet: 'dict[str, Any]') -> 'dict[str, Any]': + """Process field value after parsing (unpacked). + + Args: + value: Field value. + packet: Packet data. + + Returns: + Each subfield's value, by name, then the remainder: a member of the + field's ``flags`` where it has them, else an :class:`int`. + + """ + word = int(super().post_process(value, packet)) + out = {name: word >> start & ((1 << size) - 1) + for name, (start, size) in self._subfields.items()} # type: dict[str, Any] + rest = word & self._remainder_mask + if self._remainder is not None: + out[self._remainder] = rest if self._registry is None else self._registry(rest) + elif rest: + out[self.UNNAMED] = rest + return out diff --git a/pcapkit/protocols/data/__init__.py b/pcapkit/protocols/data/__init__.py index 953c99f545..8b5369a8e8 100644 --- a/pcapkit/protocols/data/__init__.py +++ b/pcapkit/protocols/data/__init__.py @@ -91,11 +91,12 @@ 'Radiotap_DBTXAttenuation', 'Radiotap_DBmTXPower', 'Radiotap_Antenna', 'Radiotap_DBAntennaSignal', 'Radiotap_DBAntennaNoise', 'Radiotap_RXFlags', 'Radiotap_TXFlags', 'Radiotap_RTSRetries', 'Radiotap_DataRetries', 'Radiotap_MCS', - 'Radiotap_AMPDUStatus', 'Radiotap_VHT', - 'Radiotap_Timestamp', 'Radiotap_HE', 'Radiotap_HEMU', 'Radiotap_HEMUOtherUser', + 'Radiotap_MCSFlags', 'Radiotap_AMPDUStatus', 'Radiotap_VHT', + 'Radiotap_Timestamp', 'Radiotap_HE', 'Radiotap_HEData1', 'Radiotap_HEData2', 'Radiotap_HEMU', + 'Radiotap_HEMUFlags1', 'Radiotap_HEMUFlags2', 'Radiotap_HEMUOtherUser', 'Radiotap_ZeroLengthPSDU', 'Radiotap_LSIG', 'Radiotap_TLVFields', 'Radiotap_RadiotapNamespace', 'Radiotap_VendorNamespace', 'Radiotap_S1G', 'Radiotap_USIG', - 'Radiotap_EHT', 'Radiotap_TLV', 'Radiotap_VendorTLV', + 'Radiotap_USIGCommon', 'Radiotap_EHT', 'Radiotap_TLV', 'Radiotap_VendorTLV', # 802.1Q/802.1ad VLAN Tag Types 'VLAN', 'VLAN_TCI', diff --git a/pcapkit/protocols/data/link/__init__.py b/pcapkit/protocols/data/link/__init__.py index f525193ddb..e71b3ea54b 100644 --- a/pcapkit/protocols/data/link/__init__.py +++ b/pcapkit/protocols/data/link/__init__.py @@ -59,8 +59,13 @@ from pcapkit.protocols.data.link.radiotap import DBmTXPower as Radiotap_DBmTXPower from pcapkit.protocols.data.link.radiotap import DBTXAttenuation as Radiotap_DBTXAttenuation from pcapkit.protocols.data.link.radiotap import Flags as Radiotap_Flags +from pcapkit.protocols.data.link.radiotap import HEData1 as Radiotap_HEData1 +from pcapkit.protocols.data.link.radiotap import HEData2 as Radiotap_HEData2 +from pcapkit.protocols.data.link.radiotap import HEMUFlags1 as Radiotap_HEMUFlags1 +from pcapkit.protocols.data.link.radiotap import HEMUFlags2 as Radiotap_HEMUFlags2 from pcapkit.protocols.data.link.radiotap import HEMUOtherUser as Radiotap_HEMUOtherUser from pcapkit.protocols.data.link.radiotap import LockQuality as Radiotap_LockQuality +from pcapkit.protocols.data.link.radiotap import MCSFlags as Radiotap_MCSFlags from pcapkit.protocols.data.link.radiotap import Radiotap from pcapkit.protocols.data.link.radiotap import RadiotapField as Radiotap_Field from pcapkit.protocols.data.link.radiotap import RadiotapNamespace as Radiotap_RadiotapNamespace @@ -72,6 +77,7 @@ from pcapkit.protocols.data.link.radiotap import TXAttenuation as Radiotap_TXAttenuation from pcapkit.protocols.data.link.radiotap import TXFlags as Radiotap_TXFlags from pcapkit.protocols.data.link.radiotap import Unknown as Radiotap_Unknown +from pcapkit.protocols.data.link.radiotap import USIGCommon as Radiotap_USIGCommon from pcapkit.protocols.data.link.radiotap import VendorNamespace as Radiotap_VendorNamespace from pcapkit.protocols.data.link.radiotap import VendorTLV as Radiotap_VendorTLV from pcapkit.protocols.data.link.radiotap import ZeroLengthPSDU as Radiotap_ZeroLengthPSDU @@ -116,11 +122,12 @@ 'Radiotap_DBTXAttenuation', 'Radiotap_DBmTXPower', 'Radiotap_Antenna', 'Radiotap_DBAntennaSignal', 'Radiotap_DBAntennaNoise', 'Radiotap_RXFlags', 'Radiotap_TXFlags', 'Radiotap_RTSRetries', 'Radiotap_DataRetries', 'Radiotap_MCS', - 'Radiotap_AMPDUStatus', 'Radiotap_VHT', - 'Radiotap_Timestamp', 'Radiotap_HE', 'Radiotap_HEMU', 'Radiotap_HEMUOtherUser', + 'Radiotap_MCSFlags', 'Radiotap_AMPDUStatus', 'Radiotap_VHT', + 'Radiotap_Timestamp', 'Radiotap_HE', 'Radiotap_HEData1', 'Radiotap_HEData2', 'Radiotap_HEMU', + 'Radiotap_HEMUFlags1', 'Radiotap_HEMUFlags2', 'Radiotap_HEMUOtherUser', 'Radiotap_ZeroLengthPSDU', 'Radiotap_LSIG', 'Radiotap_TLVFields', 'Radiotap_RadiotapNamespace', 'Radiotap_VendorNamespace', 'Radiotap_S1G', 'Radiotap_USIG', - 'Radiotap_EHT', 'Radiotap_TLV', 'Radiotap_VendorTLV', + 'Radiotap_USIGCommon', 'Radiotap_EHT', 'Radiotap_TLV', 'Radiotap_VendorTLV', # 802.1Q/802.1ad VLAN Tag Types 'VLAN', 'VLAN_TCI', diff --git a/pcapkit/protocols/data/link/radiotap.py b/pcapkit/protocols/data/link/radiotap.py index c5d471684d..bb86533870 100644 --- a/pcapkit/protocols/data/link/radiotap.py +++ b/pcapkit/protocols/data/link/radiotap.py @@ -13,6 +13,10 @@ from pcapkit.const.radiotap.eht_known import EHTKnown from pcapkit.const.radiotap.field_type import FieldType from pcapkit.const.radiotap.flags import Flags as Radiotap_Flags + from pcapkit.const.radiotap.he_data1_known import HEData1Known + from pcapkit.const.radiotap.he_data2_known import HEData2Known + from pcapkit.const.radiotap.he_mu_flags1_known import HEMUFlags1Known + from pcapkit.const.radiotap.he_mu_flags2_known import HEMUFlags2Known from pcapkit.const.radiotap.he_mu_other_user_known import HEMUOtherUserKnown from pcapkit.const.radiotap.lsig_data1 import LSIGData1 from pcapkit.const.radiotap.mcs_known import MCSKnown @@ -20,6 +24,7 @@ from pcapkit.const.radiotap.s1g_known import S1GKnown from pcapkit.const.radiotap.timestamp_flags import TimestampFlags from pcapkit.const.radiotap.tx_flags import TXFlags as Radiotap_TXFlags + from pcapkit.const.radiotap.usig_common_known import USIGCommonKnown from pcapkit.const.radiotap.vht_coding import VHTCoding from pcapkit.const.radiotap.vht_flags import VHTFlags from pcapkit.const.radiotap.vht_known import VHTKnown @@ -33,9 +38,10 @@ 'TSFT', 'Flags', 'Rate', 'Channel', 'FHSS', 'AntennaSignal', 'AntennaNoise', 'LockQuality', 'TXAttenuation', 'DBTXAttenuation', 'DBmTXPower', 'Antenna', 'DBAntennaSignal', 'DBAntennaNoise', 'RXFlags', 'TXFlags', 'RTSRetries', 'DataRetries', - 'MCS', 'AMPDUStatus', - 'VHT', 'Timestamp', 'HE', 'HEMU', 'HEMUOtherUser', 'ZeroLengthPSDU', 'LSIG', - 'TLVFields', 'RadiotapNamespace', 'VendorNamespace', 'S1G', 'USIG', 'EHT', + 'MCS', 'MCSFlags', 'AMPDUStatus', + 'VHT', 'Timestamp', 'HE', 'HEData1', 'HEData2', 'HEMU', 'HEMUFlags1', 'HEMUFlags2', 'HEMUOtherUser', + 'ZeroLengthPSDU', 'LSIG', + 'TLVFields', 'RadiotapNamespace', 'VendorNamespace', 'S1G', 'USIG', 'USIGCommon', 'EHT', 'TLV', 'VendorTLV', ] @@ -280,19 +286,45 @@ class DataRetries(RadiotapField): def __init__(self, type: 'FieldType', retries: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,redefined-builtin +@info_final +class MCSFlags(Data): + """Data model for the MCS field's ``flags``, each a value subfield. + + .. versionadded:: 1.6.0 + + """ + + #: Bandwidth, ``0`` for 20, ``1`` for 40, ``2`` for 20L and ``3`` for 20U. + bandwidth: 'int' + #: Guard interval, ``0`` for long and ``1`` for short. + guard_interval: 'int' + #: HT format, ``0`` for mixed and ``1`` for greenfield. + ht_format: 'int' + #: FEC type, ``0`` for BCC and ``1`` for LDPC. + fec_type: 'int' + #: Number of STBC streams. + stbc_streams: 'int' + #: Bit 0, the least significant, of the number of extension spatial streams. + ness_bit0: 'int' + + if TYPE_CHECKING: + def __init__(self, bandwidth: 'int', guard_interval: 'int', ht_format: 'int', fec_type: 'int', # pylint: disable=unused-argument,super-init-not-called,multiple-statements + stbc_streams: 'int', ness_bit0: 'int') -> 'None': ... # pylint: disable=unused-argument + + @info_final class MCS(RadiotapField): """Data model for the MCS field, bit 19.""" #: Which of :attr:`flags` and :attr:`mcs` are known. known: 'MCSKnown' - #: Bandwidth, guard interval, HT format, FEC type, STBC and Ness, a bitmap. - flags: 'int' + #: Bandwidth, guard interval, HT format, FEC type, STBC and Ness. + flags: 'MCSFlags' #: MCS rate index, as in IEEE Std 802.11n-2009. mcs: 'int' if TYPE_CHECKING: - def __init__(self, type: 'FieldType', known: 'MCSKnown', flags: 'int', mcs: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,redefined-builtin + def __init__(self, type: 'FieldType', known: 'MCSKnown', flags: 'MCSFlags', mcs: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,redefined-builtin @info_final @@ -357,14 +389,51 @@ def __init__(self, type: 'FieldType', timestamp: 'int', accuracy: 'int', # pyli unit_position: 'int', flags: 'TimestampFlags') -> 'None': ... # pylint: disable=unused-argument +@info_final +class HEData1(Data): + """Data model for the HE field's ``data1``, a value subfield and known bits. + + .. versionadded:: 1.6.0 + + """ + + #: HE PPDU format, ``0`` for HE_SU, ``1`` for HE_EXT_SU, ``2`` for HE_MU and + #: ``3`` for HE_TRIG. + ppdu_format: 'int' + #: Which of the other subfields are known. + known: 'HEData1Known' + + if TYPE_CHECKING: + def __init__(self, ppdu_format: 'int', known: 'HEData1Known') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements + + +@info_final +class HEData2(Data): + """Data model for the HE field's ``data2``, value subfields and known bits. + + .. versionadded:: 1.6.0 + + """ + + #: RU allocation offset, within the 80 MHz channel. + ru_offset: 'int' + #: The 80 MHz channel, ``0`` for the primary and ``1`` for the secondary. + pri_sec_80mhz: 'int' + #: Which of the other subfields are known. + known: 'HEData2Known' + + if TYPE_CHECKING: + def __init__(self, ru_offset: 'int', pri_sec_80mhz: 'int', known: 'HEData2Known') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements + + @info_final class HE(RadiotapField): """Data model for the HE field, bit 23, mostly the common part of HE-SIG-A.""" #: PPDU format and which of the subfields are known. - data1: 'int' - #: Which of the subfields are known, and the primary 80 MHz channel. - data2: 'int' + data1: 'HEData1' + #: RU allocation offset, the 80 MHz channel, and which subfields are known. + data2: 'HEData2' #: BSS color, beam change, UL/DL, MCS, DCM, coding, LDPC and STBC. data3: 'int' #: Spatial reuse, or STA-ID in an HE MU PPDU. @@ -375,25 +444,77 @@ class HE(RadiotapField): data6: 'int' if TYPE_CHECKING: - def __init__(self, type: 'FieldType', data1: 'int', data2: 'int', data3: 'int', # pylint: disable=unused-argument,super-init-not-called,multiple-statements,redefined-builtin + def __init__(self, type: 'FieldType', data1: 'HEData1', data2: 'HEData2', data3: 'int', # pylint: disable=unused-argument,super-init-not-called,multiple-statements,redefined-builtin data4: 'int', data5: 'int', data6: 'int') -> 'None': ... # pylint: disable=unused-argument +@info_final +class HEMUFlags1(Data): + """Data model for the HE-MU field's ``flags1``, value subfields and known bits. + + .. versionadded:: 1.6.0 + + """ + + #: SIG-B MCS, from HE-SIG-A. + sig_b_mcs: 'int' + #: SIG-B DCM, from HE-SIG-A. + sig_b_dcm: 'int' + #: Channel 1 center 26-tone RU value. + ch1_center_26_tone_ru: 'int' + #: Which of the subfields of this word and of ``flags2`` are known. + known: 'HEMUFlags1Known' + + if TYPE_CHECKING: + def __init__(self, sig_b_mcs: 'int', sig_b_dcm: 'int', ch1_center_26_tone_ru: 'int', # pylint: disable=unused-argument,super-init-not-called,multiple-statements + known: 'HEMUFlags1Known') -> 'None': ... # pylint: disable=unused-argument + + +@info_final +class HEMUFlags2(Data): + """Data model for the HE-MU field's ``flags2``, value subfields and known bits. + + .. versionadded:: 1.6.0 + + """ + + #: Bandwidth from the Bandwidth field of HE-SIG-A, ``0`` for 20 MHz up to + #: ``3`` for 160 or 80+80 MHz. + bandwidth: 'int' + #: SIG-B compression, from HE-SIG-A. + sig_b_compression: 'int' + #: Number of HE-SIG-B symbols or MU-MIMO users, less one, from HE-SIG-A. + sig_b_symbols_users: 'int' + #: Preamble puncturing, from the Bandwidth field of HE-SIG-A. + preamble_puncturing: 'int' + #: Channel 2 center 26-tone RU value. + ch2_center_26_tone_ru: 'int' + #: Which of the bandwidth and preamble puncturing are known. + known: 'HEMUFlags2Known' + + if TYPE_CHECKING: + def __init__(self, bandwidth: 'int', sig_b_compression: 'int', sig_b_symbols_users: 'int', # pylint: disable=unused-argument,super-init-not-called,multiple-statements + preamble_puncturing: 'int', ch2_center_26_tone_ru: 'int', # pylint: disable=unused-argument + known: 'HEMUFlags2Known') -> 'None': ... # pylint: disable=unused-argument + + @info_final class HEMU(RadiotapField): """Data model for the HE-MU field, bit 24, from HE-SIG-A and HE-SIG-B.""" - #: SIG-B MCS, DCM, compression and which of the subfields are known. - flags1: 'int' - #: Bandwidth, SIG-B symbols or MU-MIMO users, and punctured preamble. - flags2: 'int' + #: SIG-B MCS and DCM, the channel 1 center 26-tone RU, and which subfields + #: are known. + flags1: 'HEMUFlags1' + #: Bandwidth, SIG-B compression, SIG-B symbols or MU-MIMO users, preamble + #: puncturing and the channel 2 center 26-tone RU. + flags2: 'HEMUFlags2' #: RU_channel1, one octet per RU. ru_channel1: 'tuple[int, int, int, int]' #: RU_channel2, one octet per RU. ru_channel2: 'tuple[int, int, int, int]' if TYPE_CHECKING: - def __init__(self, type: 'FieldType', flags1: 'int', flags2: 'int', # pylint: disable=unused-argument,super-init-not-called,multiple-statements,redefined-builtin + def __init__(self, type: 'FieldType', flags1: 'HEMUFlags1', flags2: 'HEMUFlags2', # pylint: disable=unused-argument,super-init-not-called,multiple-statements,redefined-builtin ru_channel1: 'tuple[int, int, int, int]', # pylint: disable=unused-argument ru_channel2: 'tuple[int, int, int, int]') -> 'None': ... # pylint: disable=unused-argument @@ -527,19 +648,47 @@ class S1G(RadiotapField): def __init__(self, type: 'FieldType', known: 'S1GKnown', data1: 'int', data2: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,redefined-builtin +@info_final +class USIGCommon(Data): + """Data model for the U-SIG field's ``common``, value subfields and known bits. + + .. versionadded:: 1.6.0 + + """ + + #: PHY version identifier. + phy_version: 'int' + #: Bandwidth, ``0`` for 20 MHz up to ``3`` for 160 MHz, and ``4`` and ``5`` for + #: the two 320 MHz channelizations. + bandwidth: 'int' + #: UL/DL. + ul_dl: 'int' + #: BSS color. + bss_color: 'int' + #: TXOP. + txop: 'int' + #: Which of the subfields are known, and whether the U-SIG CRC failed and its + #: validate bits were checked and found right. + known: 'USIGCommonKnown' + + if TYPE_CHECKING: + def __init__(self, phy_version: 'int', bandwidth: 'int', ul_dl: 'int', bss_color: 'int', txop: 'int', # pylint: disable=unused-argument,super-init-not-called,multiple-statements + known: 'USIGCommonKnown') -> 'None': ... # pylint: disable=unused-argument + + @info_final class USIG(RadiotapField): """Data model for the U-SIG field, TLV type 33.""" - #: Which of the common subfields are known, and their values. - common: 'int' + #: The common subfields, and which are known. + common: 'USIGCommon' #: Version-dependent subfields. value: 'int' #: Which bits of :attr:`value` are known. mask: 'int' if TYPE_CHECKING: - def __init__(self, type: 'FieldType', common: 'int', value: 'int', mask: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,redefined-builtin + def __init__(self, type: 'FieldType', common: 'USIGCommon', value: 'int', mask: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements,redefined-builtin @info_final diff --git a/pcapkit/protocols/link/radiotap.py b/pcapkit/protocols/link/radiotap.py index cd5def914f..24eb94f771 100644 --- a/pcapkit/protocols/link/radiotap.py +++ b/pcapkit/protocols/link/radiotap.py @@ -77,7 +77,11 @@ size alike; OpenBSD writes a 2-octet RSSI field at bit 16 instead, which is not read. A bitmap radiotap.org defines bit by bit is a member of its flag registry under :mod:`pcapkit.const.radiotap`, which keeps the bits it does not name. A -field type registered through +bitmap that mixes value subfields with known bits -- the MCS ``flags``, the HE +``data1`` and ``data2``, the HE-MU ``flags1`` and ``flags2`` and the U-SIG +``common`` -- is split: each value subfield is an integer, and its other bits, +the undefined ones among them, a member of its known-bits registry. A field +type registered through :func:`~pcapkit.foundation.registry.protocols.link.register_radiotap_field` is decoded the same way. @@ -147,8 +151,13 @@ from pcapkit.protocols.data.link.radiotap import DBmTXPower as Data_DBmTXPower from pcapkit.protocols.data.link.radiotap import DBTXAttenuation as Data_DBTXAttenuation from pcapkit.protocols.data.link.radiotap import Flags as Data_Flags +from pcapkit.protocols.data.link.radiotap import HEData1 as Data_HEData1 +from pcapkit.protocols.data.link.radiotap import HEData2 as Data_HEData2 +from pcapkit.protocols.data.link.radiotap import HEMUFlags1 as Data_HEMUFlags1 +from pcapkit.protocols.data.link.radiotap import HEMUFlags2 as Data_HEMUFlags2 from pcapkit.protocols.data.link.radiotap import HEMUOtherUser as Data_HEMUOtherUser from pcapkit.protocols.data.link.radiotap import LockQuality as Data_LockQuality +from pcapkit.protocols.data.link.radiotap import MCSFlags as Data_MCSFlags from pcapkit.protocols.data.link.radiotap import Radiotap as Data_Radiotap from pcapkit.protocols.data.link.radiotap import RadiotapNamespace as Data_RadiotapNamespace from pcapkit.protocols.data.link.radiotap import Rate as Data_Rate @@ -159,6 +168,7 @@ from pcapkit.protocols.data.link.radiotap import TXAttenuation as Data_TXAttenuation from pcapkit.protocols.data.link.radiotap import TXFlags as Data_TXFlags from pcapkit.protocols.data.link.radiotap import Unknown as Data_Unknown +from pcapkit.protocols.data.link.radiotap import USIGCommon as Data_USIGCommon from pcapkit.protocols.data.link.radiotap import VendorNamespace as Data_VendorNamespace from pcapkit.protocols.data.link.radiotap import VendorTLV as Data_VendorTLV from pcapkit.protocols.data.link.radiotap import ZeroLengthPSDU as Data_ZeroLengthPSDU @@ -211,6 +221,7 @@ from typing_extensions import Literal from pcapkit.corekit.enum import EnumRegistry + from pcapkit.corekit.fields.numbers import BitmapField from pcapkit.protocols.data.link.radiotap import RadiotapField as Data_Field #: A field given to :meth:`Radiotap.make`: a data model, or a ``(type, mapping)`` pair. @@ -1067,7 +1078,8 @@ def _read_field_mcs(self, field_type: 'Enum_FieldType', schema: 'Schema_MCS') -> Parsed field data. """ - return Data_MCS(type=field_type, known=schema.known, flags=schema.flags, mcs=schema.mcs) + return Data_MCS(type=field_type, known=schema.known, flags=Data_MCSFlags(**schema.flags), + mcs=schema.mcs) def _read_field_ampdu_status(self, field_type: 'Enum_FieldType', schema: 'Schema_AMPDUStatus') -> 'Data_AMPDUStatus': @@ -1125,7 +1137,8 @@ def _read_field_he(self, field_type: 'Enum_FieldType', schema: 'Schema_HE') -> ' Parsed field data. """ - return Data_HE(type=field_type, data1=schema.data1, data2=schema.data2, data3=schema.data3, + return Data_HE(type=field_type, data1=Data_HEData1(**schema.data1), data2=Data_HEData2(**schema.data2), + data3=schema.data3, data4=schema.data4, data5=schema.data5, data6=schema.data6) def _read_field_he_mu(self, field_type: 'Enum_FieldType', schema: 'Schema_HEMU') -> 'Data_HEMU': @@ -1139,7 +1152,8 @@ def _read_field_he_mu(self, field_type: 'Enum_FieldType', schema: 'Schema_HEMU') Parsed field data. """ - return Data_HEMU(type=field_type, flags1=schema.flags1, flags2=schema.flags2, + return Data_HEMU(type=field_type, flags1=Data_HEMUFlags1(**schema.flags1), + flags2=Data_HEMUFlags2(**schema.flags2), ru_channel1=cast('tuple[int, int, int, int]', tuple(schema.ru_channel1)), ru_channel2=cast('tuple[int, int, int, int]', tuple(schema.ru_channel2))) @@ -1210,7 +1224,8 @@ def _read_field_usig(self, field_type: 'Enum_FieldType', schema: 'Schema_USIG') Parsed field data. """ - return Data_USIG(type=field_type, common=schema.common, value=schema.value, mask=schema.mask) + return Data_USIG(type=field_type, common=Data_USIGCommon(**schema.common), value=schema.value, + mask=schema.mask) def _read_field_eht(self, field_type: 'Enum_FieldType', schema: 'Schema_EHT') -> 'Data_EHT': """Read an EHT field. @@ -1523,7 +1538,7 @@ def _make_field_mcs(self, field_type: 'Enum_FieldType', entry: 'Data_MCS | Mappi """ return Schema_MCS( known=self._make_subfield(field_type, Schema_MCS, entry, 'known'), - flags=self._make_subfield(field_type, Schema_MCS, entry, 'flags'), + flags=self._make_subfield_bitmap(field_type, Schema_MCS, entry, 'flags'), mcs=self._make_subfield(field_type, Schema_MCS, entry, 'mcs'), ) @@ -1598,8 +1613,8 @@ def _make_field_he(self, field_type: 'Enum_FieldType', entry: 'Data_HE | Mapping """ return Schema_HE( - data1=self._make_subfield(field_type, Schema_HE, entry, 'data1'), - data2=self._make_subfield(field_type, Schema_HE, entry, 'data2'), + data1=self._make_subfield_bitmap(field_type, Schema_HE, entry, 'data1'), + data2=self._make_subfield_bitmap(field_type, Schema_HE, entry, 'data2'), data3=self._make_subfield(field_type, Schema_HE, entry, 'data3'), data4=self._make_subfield(field_type, Schema_HE, entry, 'data4'), data5=self._make_subfield(field_type, Schema_HE, entry, 'data5'), @@ -1619,8 +1634,8 @@ def _make_field_he_mu(self, field_type: 'Enum_FieldType', """ return Schema_HEMU( - flags1=self._make_subfield(field_type, Schema_HEMU, entry, 'flags1'), - flags2=self._make_subfield(field_type, Schema_HEMU, entry, 'flags2'), + flags1=self._make_subfield_bitmap(field_type, Schema_HEMU, entry, 'flags1'), + flags2=self._make_subfield_bitmap(field_type, Schema_HEMU, entry, 'flags2'), ru_channel1=self._make_subfield_list(field_type, Schema_HEMU, entry, 'ru_channel1'), ru_channel2=self._make_subfield_list(field_type, Schema_HEMU, entry, 'ru_channel2'), ) @@ -1705,7 +1720,7 @@ def _make_field_usig(self, field_type: 'Enum_FieldType', entry: 'Data_USIG | Map """ return Schema_USIG( - common=self._make_subfield(field_type, Schema_USIG, entry, 'common'), + common=self._make_subfield_bitmap(field_type, Schema_USIG, entry, 'common'), value=self._make_subfield(field_type, Schema_USIG, entry, 'value'), mask=self._make_subfield(field_type, Schema_USIG, entry, 'mask'), ) @@ -1917,6 +1932,46 @@ def _make_subfield_list(self, field_type: 'Enum_FieldType', schema: 'Type[Schema f'must have {field.length // item.length} values') return tuple(self._make_uint(number, bits=8 * item.length, name=what) for number in value) + def _make_subfield_bitmap(self, field_type: 'Enum_FieldType', schema: 'Type[Schema]', + entry: 'Mapping[str, Any]', name: 'str') -> 'Any': + """Check a bitmap subfield of ``entry`` against the field of ``schema`` that holds it. + + Args: + field_type: type of the field, for the error message + schema: field schema + entry: a data model or a mapping holding the subfield + name: name of the subfield + + Returns: + The value of each of the bitmap's subfields, and of its known bits, + as the schema's typed dictionary for the bitmap. + + Raises: + ProtocolError: If the subfield is missing or is not a mapping, one of + its own is missing or does not fit in its bits, or its known bits + set a bit of another. + + """ + what = f'{field_type.name}.{name}' + if name not in entry: + raise ProtocolError(f'{self.alias}: invalid fields: {what} missing') + value = entry[name] + field = cast('BitmapField', schema.__fields__[name]) + if not isinstance(value, collections.abc.Mapping): + raise ProtocolError(f'{self.alias}: invalid {what}: {value!r}, must be a mapping of its subfields') + names = dict(field.subfields) + if field.remainder is not None: + names[field.remainder] = (0, 8 * field.length) + out = {} # type: dict[str, int] + for sub, (_, size) in names.items(): + if sub not in value: + raise ProtocolError(f'{self.alias}: invalid fields: {what}.{sub} missing') + out[sub] = self._make_uint(value[sub], bits=size, name=f'{what}.{sub}') + if field.remainder is not None and out[field.remainder] & ~field.remainder_mask: + raise ProtocolError(f'{self.alias}: invalid {what}.{field.remainder}: {value[field.remainder]!r}, ' + 'sets a bit of another subfield') + return out + def _read_vendor_namespace(self, field_type: 'Enum_FieldType', fields: 'bytes', pos: 'int', start: 'int') -> 'Optional[tuple[Data_VendorNamespace, int]]': """Decode a Vendor Namespace field and its vendor data. diff --git a/pcapkit/protocols/schema/link/radiotap.py b/pcapkit/protocols/schema/link/radiotap.py index 3f268ddb3c..8c9afc98e2 100644 --- a/pcapkit/protocols/schema/link/radiotap.py +++ b/pcapkit/protocols/schema/link/radiotap.py @@ -9,6 +9,10 @@ from pcapkit.const.radiotap.eht_known import EHTKnown as Enum_EHTKnown from pcapkit.const.radiotap.field_type import FieldType as Enum_FieldType from pcapkit.const.radiotap.flags import Flags as Enum_Flags +from pcapkit.const.radiotap.he_data1_known import HEData1Known as Enum_HEData1Known +from pcapkit.const.radiotap.he_data2_known import HEData2Known as Enum_HEData2Known +from pcapkit.const.radiotap.he_mu_flags1_known import HEMUFlags1Known as Enum_HEMUFlags1Known +from pcapkit.const.radiotap.he_mu_flags2_known import HEMUFlags2Known as Enum_HEMUFlags2Known from pcapkit.const.radiotap.he_mu_other_user_known import \ HEMUOtherUserKnown as Enum_HEMUOtherUserKnown from pcapkit.const.radiotap.lsig_data1 import LSIGData1 as Enum_LSIGData1 @@ -17,13 +21,14 @@ from pcapkit.const.radiotap.s1g_known import S1GKnown as Enum_S1GKnown from pcapkit.const.radiotap.timestamp_flags import TimestampFlags as Enum_TimestampFlags from pcapkit.const.radiotap.tx_flags import TXFlags as Enum_TXFlags +from pcapkit.const.radiotap.usig_common_known import USIGCommonKnown as Enum_USIGCommonKnown from pcapkit.const.radiotap.vht_coding import VHTCoding as Enum_VHTCoding from pcapkit.const.radiotap.vht_flags import VHTFlags as Enum_VHTFlags from pcapkit.const.radiotap.vht_known import VHTKnown as Enum_VHTKnown from pcapkit.corekit.fields.collections import ListField from pcapkit.corekit.fields.misc import PayloadField -from pcapkit.corekit.fields.numbers import (EnumField, Int8Field, UInt8Field, UInt16Field, - UInt32Field, UInt64Field) +from pcapkit.corekit.fields.numbers import (BitmapField, EnumField, Int8Field, UInt8Field, + UInt16Field, UInt32Field, UInt64Field) from pcapkit.corekit.fields.strings import BytesField from pcapkit.protocols.schema.schema import EnumSchema, Schema, schema_final @@ -41,11 +46,12 @@ ] if TYPE_CHECKING: - from typing import ClassVar + from typing import Any, ClassVar from pcapkit.protocols.protocol import ProtocolBase + @schema_final class Radiotap(Schema): """Header schema for an IEEE 802.11 radiotap header. @@ -337,13 +343,21 @@ class MCS(RadiotapField, code=Enum_FieldType.MCS): #: Which of :attr:`flags` and :attr:`mcs` are known. known: 'Enum_MCSKnown' = EnumField(length=1, namespace=Enum_MCSKnown) - #: Flags, which hold value subfields. - flags: 'int' = UInt8Field() + #: Bandwidth, guard interval, HT format, FEC type, STBC streams and the + #: low bit of the extension spatial streams. + flags: 'dict[str, Any]' = BitmapField(length=1, namespace={ + 'bandwidth': (0, 2), + 'guard_interval': (2, 1), + 'ht_format': (3, 1), + 'fec_type': (4, 1), + 'stbc_streams': (5, 2), + 'ness_bit0': (7, 1), + }) #: MCS rate index. mcs: 'int' = UInt8Field() if TYPE_CHECKING: - def __init__(self, known: 'Enum_MCSKnown | int', flags: 'int', mcs: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements + def __init__(self, known: 'Enum_MCSKnown | int', flags: 'dict[str, Any]', mcs: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements @schema_final @@ -419,10 +433,16 @@ class HE(RadiotapField, code=Enum_FieldType.HE): __align__ = 2 - #: Format and which other subfields are known. - data1: 'int' = UInt16Field(byteorder='little') - #: Second data word. - data2: 'int' = UInt16Field(byteorder='little') + #: PPDU format, and which other subfields are known. + data1: 'dict[str, Any]' = BitmapField(length=2, byteorder='little', namespace={ + 'ppdu_format': (0, 2), + }, remainder='known', flags=Enum_HEData1Known) + #: RU allocation offset, primary or secondary 80 MHz, and which other + #: subfields are known. + data2: 'dict[str, Any]' = BitmapField(length=2, byteorder='little', namespace={ + 'ru_offset': (8, 6), + 'pri_sec_80mhz': (15, 1), + }, remainder='known', flags=Enum_HEData2Known) #: Third data word. data3: 'int' = UInt16Field(byteorder='little') #: Fourth data word. @@ -433,7 +453,7 @@ class HE(RadiotapField, code=Enum_FieldType.HE): data6: 'int' = UInt16Field(byteorder='little') if TYPE_CHECKING: - def __init__(self, data1: 'int', data2: 'int', data3: 'int', data4: 'int', data5: 'int', # pylint: disable=unused-argument,super-init-not-called,multiple-statements + def __init__(self, data1: 'dict[str, Any]', data2: 'dict[str, Any]', data3: 'int', data4: 'int', data5: 'int', # pylint: disable=unused-argument,super-init-not-called,multiple-statements data6: 'int') -> 'None': ... # pylint: disable=unused-argument @@ -443,17 +463,30 @@ class HEMU(RadiotapField, code=Enum_FieldType.HE_MU): __align__ = 2 - #: First flags word. - flags1: 'int' = UInt16Field(byteorder='little') - #: Second flags word. - flags2: 'int' = UInt16Field(byteorder='little') + #: SIG-B MCS and DCM, the channel 1 center 26-tone RU, and which subfields + #: are known. + flags1: 'dict[str, Any]' = BitmapField(length=2, byteorder='little', namespace={ + 'sig_b_mcs': (0, 4), + 'sig_b_dcm': (5, 1), + 'ch1_center_26_tone_ru': (13, 1), + }, remainder='known', flags=Enum_HEMUFlags1Known) + #: Bandwidth, SIG-B compression, SIG-B symbols or MU-MIMO users, preamble + #: puncturing, the channel 2 center 26-tone RU, and which are known. + flags2: 'dict[str, Any]' = BitmapField(length=2, byteorder='little', namespace={ + 'bandwidth': (0, 2), + 'sig_b_compression': (3, 1), + 'sig_b_symbols_users': (4, 4), + 'preamble_puncturing': (8, 2), + 'ch2_center_26_tone_ru': (11, 1), + }, remainder='known', flags=Enum_HEMUFlags2Known) #: RU allocation of channel 1. ru_channel1: 'list[int]' = ListField(length=4, item_type=UInt8Field()) #: RU allocation of channel 2. ru_channel2: 'list[int]' = ListField(length=4, item_type=UInt8Field()) if TYPE_CHECKING: - def __init__(self, flags1: 'int', flags2: 'int', ru_channel1: 'list[int] | tuple[int, ...]', # pylint: disable=unused-argument,super-init-not-called,multiple-statements + def __init__(self, flags1: 'dict[str, Any]', flags2: 'dict[str, Any]', # pylint: disable=unused-argument,super-init-not-called,multiple-statements + ru_channel1: 'list[int] | tuple[int, ...]', # pylint: disable=unused-argument ru_channel2: 'list[int] | tuple[int, ...]') -> 'None': ... # pylint: disable=unused-argument @@ -549,15 +582,21 @@ class USIG(RadiotapField, code=Enum_FieldType.U_SIG): __align__ = 4 - #: Common subfields, which hold value subfields. - common: 'int' = UInt32Field(byteorder='little') + #: PHY version, bandwidth, UL/DL, BSS color and TXOP, and which are known. + common: 'dict[str, Any]' = BitmapField(length=4, byteorder='little', namespace={ + 'phy_version': (12, 3), + 'bandwidth': (15, 3), + 'ul_dl': (18, 1), + 'bss_color': (19, 6), + 'txop': (25, 7), + }, remainder='known', flags=Enum_USIGCommonKnown) #: Value of the U-SIG bits. value: 'int' = UInt32Field(byteorder='little') #: Which of :attr:`value`'s bits are known. mask: 'int' = UInt32Field(byteorder='little') if TYPE_CHECKING: - def __init__(self, common: 'int', value: 'int', mask: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements + def __init__(self, common: 'dict[str, Any]', value: 'int', mask: 'int') -> 'None': ... # pylint: disable=unused-argument,super-init-not-called,multiple-statements @schema_final diff --git a/pcapkit/vendor/__init__.py b/pcapkit/vendor/__init__.py index 58756d5247..8dc77bb89d 100644 --- a/pcapkit/vendor/__init__.py +++ b/pcapkit/vendor/__init__.py @@ -199,8 +199,10 @@ def __getattr__(name: 'str') -> 'typing.NoReturn': # Radiotap 'Radiotap_FieldType', 'Radiotap_Flags', 'Radiotap_ChannelFlags', 'Radiotap_RXFlags', 'Radiotap_TXFlags', 'Radiotap_MCSKnown', 'Radiotap_AMPDUFlags', 'Radiotap_VHTKnown', - 'Radiotap_VHTFlags', 'Radiotap_VHTCoding', 'Radiotap_TimestampFlags', - 'Radiotap_HEMUOtherUserKnown', 'Radiotap_LSIGData1', 'Radiotap_S1GKnown', 'Radiotap_EHTKnown', + 'Radiotap_VHTFlags', 'Radiotap_VHTCoding', 'Radiotap_TimestampFlags', 'Radiotap_HEData1Known', + 'Radiotap_HEData2Known', 'Radiotap_HEMUFlags1Known', 'Radiotap_HEMUFlags2Known', + 'Radiotap_HEMUOtherUserKnown', 'Radiotap_LSIGData1', 'Radiotap_S1GKnown', + 'Radiotap_USIGCommonKnown', 'Radiotap_EHTKnown', # SCTP 'SCTP_Chunk', 'SCTP_Parameter', 'SCTP_CauseCode', 'SCTP_PayloadProtocolIdentifier', diff --git a/pcapkit/vendor/radiotap/__init__.py b/pcapkit/vendor/radiotap/__init__.py index 427db46ffc..87c4bd5464 100644 --- a/pcapkit/vendor/radiotap/__init__.py +++ b/pcapkit/vendor/radiotap/__init__.py @@ -33,12 +33,22 @@ - VHT Coding Flags * - :class:`Radiotap_TimestampFlags ` - Timestamp Flags + * - :class:`Radiotap_HEData1Known ` + - HE Data1 Known Flags + * - :class:`Radiotap_HEData2Known ` + - HE Data2 Known Flags + * - :class:`Radiotap_HEMUFlags1Known ` + - HE-MU Flags1 Known Flags + * - :class:`Radiotap_HEMUFlags2Known ` + - HE-MU Flags2 Known Flags * - :class:`Radiotap_HEMUOtherUserKnown ` - HE-MU-other-user Known Flags * - :class:`Radiotap_LSIGData1 ` - L-SIG Data1 Flags * - :class:`Radiotap_S1GKnown ` - S1G Known Flags + * - :class:`Radiotap_USIGCommonKnown ` + - U-SIG Common Known Flags * - :class:`Radiotap_EHTKnown ` - EHT Known Flags @@ -49,6 +59,10 @@ from pcapkit.vendor.radiotap.eht_known import EHTKnown as Radiotap_EHTKnown from pcapkit.vendor.radiotap.field_type import FieldType as Radiotap_FieldType from pcapkit.vendor.radiotap.flags import Flags as Radiotap_Flags +from pcapkit.vendor.radiotap.he_data1_known import HEData1Known as Radiotap_HEData1Known +from pcapkit.vendor.radiotap.he_data2_known import HEData2Known as Radiotap_HEData2Known +from pcapkit.vendor.radiotap.he_mu_flags1_known import HEMUFlags1Known as Radiotap_HEMUFlags1Known +from pcapkit.vendor.radiotap.he_mu_flags2_known import HEMUFlags2Known as Radiotap_HEMUFlags2Known from pcapkit.vendor.radiotap.he_mu_other_user_known import \ HEMUOtherUserKnown as Radiotap_HEMUOtherUserKnown from pcapkit.vendor.radiotap.lsig_data1 import LSIGData1 as Radiotap_LSIGData1 @@ -57,6 +71,7 @@ from pcapkit.vendor.radiotap.s1g_known import S1GKnown as Radiotap_S1GKnown from pcapkit.vendor.radiotap.timestamp_flags import TimestampFlags as Radiotap_TimestampFlags from pcapkit.vendor.radiotap.tx_flags import TXFlags as Radiotap_TXFlags +from pcapkit.vendor.radiotap.usig_common_known import USIGCommonKnown as Radiotap_USIGCommonKnown from pcapkit.vendor.radiotap.vht_coding import VHTCoding as Radiotap_VHTCoding from pcapkit.vendor.radiotap.vht_flags import VHTFlags as Radiotap_VHTFlags from pcapkit.vendor.radiotap.vht_known import VHTKnown as Radiotap_VHTKnown @@ -64,6 +79,8 @@ __all__ = [ 'Radiotap_FieldType', 'Radiotap_Flags', 'Radiotap_ChannelFlags', 'Radiotap_RXFlags', 'Radiotap_TXFlags', 'Radiotap_MCSKnown', 'Radiotap_AMPDUFlags', 'Radiotap_VHTKnown', - 'Radiotap_VHTFlags', 'Radiotap_VHTCoding', 'Radiotap_TimestampFlags', - 'Radiotap_HEMUOtherUserKnown', 'Radiotap_LSIGData1', 'Radiotap_S1GKnown', 'Radiotap_EHTKnown', + 'Radiotap_VHTFlags', 'Radiotap_VHTCoding', 'Radiotap_TimestampFlags', 'Radiotap_HEData1Known', + 'Radiotap_HEData2Known', 'Radiotap_HEMUFlags1Known', 'Radiotap_HEMUFlags2Known', + 'Radiotap_HEMUOtherUserKnown', 'Radiotap_LSIGData1', 'Radiotap_S1GKnown', + 'Radiotap_USIGCommonKnown', 'Radiotap_EHTKnown', ] diff --git a/pcapkit/vendor/radiotap/flags.py b/pcapkit/vendor/radiotap/flags.py index 41e6812b26..606e3d0e0c 100644 --- a/pcapkit/vendor/radiotap/flags.py +++ b/pcapkit/vendor/radiotap/flags.py @@ -16,8 +16,9 @@ A table row whose mask sets one bit is a member. A row that sets more bits, or that radiotap.org marks reserved or unused, is not: its bits stay undefined, which an ``aenum.IntFlag`` keeps in a value rather than dropping, so they -round-trip. The Flags page lists its last bit in a table of its own, which the -member's comment names. +round-trip. Nor is a row of :attr:`~Flags.VALUES`, a value subfield of a bitmap +that mixes them with flags, which its schema splits off. The Flags page lists +its last bit in a table of its own, which the member's comment names. A member's value and comment come from radiotap.org, and its name from the crawler's :attr:`~Flags.NAMES`: the macro the Linux kernel's @@ -146,6 +147,9 @@ class Flags(Vendor): #: Subfield whose table is scraped, as the page names it before the table; #: :obj:`None` for every table on the page. SUBFIELD = None # type: Optional[str] + #: Masks of the value subfields the bitmap mixes with its flags, whose rows + #: are no members. + VALUES = () # type: tuple[int, ...] #: The source macro of each bit, keyed by mask, which is Linux's for ``0x01`` #: to ``0x40``, and Wireshark's ``packet-ieee80211-radiotap.c`` for ``0x80``, #: which Linux leaves unnamed. @@ -270,7 +274,7 @@ def _members(self, data: 'list[Table]') -> 'list[tuple[int, str, str]]': if not MASK.fullmatch(row[0]): continue mask = int(row[0], 16) - if mask & (mask - 1): + if mask & (mask - 1) or mask in self.VALUES: continue meanings = [(column, text) for column, text in zip(header[1:], row[1:]) if column.lower() != 'notes'] diff --git a/pcapkit/vendor/radiotap/he_data1_known.py b/pcapkit/vendor/radiotap/he_data1_known.py new file mode 100644 index 0000000000..8713a23a34 --- /dev/null +++ b/pcapkit/vendor/radiotap/he_data1_known.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +"""Radiotap HE Data1 Known Flags +=================================== + +.. module:: pcapkit.vendor.radiotap.he_data1_known + +This module contains the vendor crawler for **Radiotap HE Data1 Known Flags**, +which is automatically generating :class:`pcapkit.const.radiotap.he_data1_known.HEData1Known`. + +The registry is the bits of the HE field's ``data1`` besides its value subfields, as radiotap.org +tables them; see :mod:`pcapkit.vendor.radiotap.flags` for how a row becomes a +member, and where its name comes from. + +""" + +import sys + +from pcapkit.vendor.radiotap.flags import Flags + +__all__ = ['HEData1Known'] + + +class HEData1Known(Flags): + """Radiotap HE Data1 Known Flags""" + + #: Value limit checker. + FLAG = 'isinstance(value, int) and 0 <= value <= 0xFFFF' + #: Link to registry. + LINK = 'https://www.radiotap.org/fields/HE.html' + #: Subfield whose table is scraped. + SUBFIELD = 'data1' + #: Masks of the value subfields, which the schema splits off. + VALUES = (0x0003,) # type: tuple[int, ...] + #: The source macro of each bit, keyed by mask, which is Linux's. + NAMES = { + 0x0004: 'IEEE80211_RADIOTAP_HE_DATA1_BSS_COLOR_KNOWN', + 0x0008: 'IEEE80211_RADIOTAP_HE_DATA1_BEAM_CHANGE_KNOWN', + 0x0010: 'IEEE80211_RADIOTAP_HE_DATA1_UL_DL_KNOWN', + 0x0020: 'IEEE80211_RADIOTAP_HE_DATA1_DATA_MCS_KNOWN', + 0x0040: 'IEEE80211_RADIOTAP_HE_DATA1_DATA_DCM_KNOWN', + 0x0080: 'IEEE80211_RADIOTAP_HE_DATA1_CODING_KNOWN', + 0x0100: 'IEEE80211_RADIOTAP_HE_DATA1_LDPC_XSYMSEG_KNOWN', + 0x0200: 'IEEE80211_RADIOTAP_HE_DATA1_STBC_KNOWN', + 0x0400: 'IEEE80211_RADIOTAP_HE_DATA1_SPTL_REUSE_KNOWN', + 0x0800: 'IEEE80211_RADIOTAP_HE_DATA1_SPTL_REUSE2_KNOWN', + 0x1000: 'IEEE80211_RADIOTAP_HE_DATA1_SPTL_REUSE3_KNOWN', + 0x2000: 'IEEE80211_RADIOTAP_HE_DATA1_SPTL_REUSE4_KNOWN', + 0x4000: 'IEEE80211_RADIOTAP_HE_DATA1_BW_RU_ALLOC_KNOWN', + 0x8000: 'IEEE80211_RADIOTAP_HE_DATA1_DOPPLER_KNOWN', + } # type: dict[int, str] + + +if __name__ == '__main__': + sys.exit(HEData1Known()) # type: ignore[arg-type] diff --git a/pcapkit/vendor/radiotap/he_data2_known.py b/pcapkit/vendor/radiotap/he_data2_known.py new file mode 100644 index 0000000000..740925c902 --- /dev/null +++ b/pcapkit/vendor/radiotap/he_data2_known.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +"""Radiotap HE Data2 Known Flags +=================================== + +.. module:: pcapkit.vendor.radiotap.he_data2_known + +This module contains the vendor crawler for **Radiotap HE Data2 Known Flags**, +which is automatically generating :class:`pcapkit.const.radiotap.he_data2_known.HEData2Known`. + +The registry is the bits of the HE field's ``data2`` besides its value subfields, as radiotap.org +tables them; see :mod:`pcapkit.vendor.radiotap.flags` for how a row becomes a +member, and where its name comes from. + +""" + +import sys + +from pcapkit.vendor.radiotap.flags import Flags + +__all__ = ['HEData2Known'] + + +class HEData2Known(Flags): + """Radiotap HE Data2 Known Flags""" + + #: Value limit checker. + FLAG = 'isinstance(value, int) and 0 <= value <= 0xFFFF' + #: Link to registry. + LINK = 'https://www.radiotap.org/fields/HE.html' + #: Subfield whose table is scraped. + SUBFIELD = 'data2' + #: Masks of the value subfields, which the schema splits off. + VALUES = (0x3F00, 0x8000,) # type: tuple[int, ...] + #: The source macro of each bit, keyed by mask, which is Linux's. + NAMES = { + 0x0001: 'IEEE80211_RADIOTAP_HE_DATA2_PRISEC_80_KNOWN', + 0x0002: 'IEEE80211_RADIOTAP_HE_DATA2_GI_KNOWN', + 0x0004: 'IEEE80211_RADIOTAP_HE_DATA2_NUM_LTF_SYMS_KNOWN', + 0x0008: 'IEEE80211_RADIOTAP_HE_DATA2_PRE_FEC_PAD_KNOWN', + 0x0010: 'IEEE80211_RADIOTAP_HE_DATA2_TXBF_KNOWN', + 0x0020: 'IEEE80211_RADIOTAP_HE_DATA2_PE_DISAMBIG_KNOWN', + 0x0040: 'IEEE80211_RADIOTAP_HE_DATA2_TXOP_KNOWN', + 0x0080: 'IEEE80211_RADIOTAP_HE_DATA2_MIDAMBLE_KNOWN', + 0x4000: 'IEEE80211_RADIOTAP_HE_DATA2_RU_OFFSET_KNOWN', + } # type: dict[int, str] + + +if __name__ == '__main__': + sys.exit(HEData2Known()) # type: ignore[arg-type] diff --git a/pcapkit/vendor/radiotap/he_mu_flags1_known.py b/pcapkit/vendor/radiotap/he_mu_flags1_known.py new file mode 100644 index 0000000000..9e8ad783c5 --- /dev/null +++ b/pcapkit/vendor/radiotap/he_mu_flags1_known.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +"""Radiotap HE-MU Flags1 Known Flags +======================================= + +.. module:: pcapkit.vendor.radiotap.he_mu_flags1_known + +This module contains the vendor crawler for **Radiotap HE-MU Flags1 Known Flags**, +which is automatically generating :class:`pcapkit.const.radiotap.he_mu_flags1_known.HEMUFlags1Known`. + +The registry is the bits of the HE-MU field's ``flags1`` besides its value subfields, as radiotap.org +tables them; see :mod:`pcapkit.vendor.radiotap.flags` for how a row becomes a +member, and where its name comes from. + +""" + +import sys + +from pcapkit.vendor.radiotap.flags import Flags + +__all__ = ['HEMUFlags1Known'] + + +class HEMUFlags1Known(Flags): + """Radiotap HE-MU Flags1 Known Flags""" + + #: Value limit checker. + FLAG = 'isinstance(value, int) and 0 <= value <= 0xFFFF' + #: Link to registry. + LINK = 'https://www.radiotap.org/fields/HE-MU.html' + #: Subfield whose table is scraped. + SUBFIELD = 'flags1' + #: Masks of the value subfields, which the schema splits off. + VALUES = (0x000F, 0x0020, 0x2000,) # type: tuple[int, ...] + #: The source macro of each bit, keyed by mask, which is Linux's. + NAMES = { + 0x0010: 'IEEE80211_RADIOTAP_HE_MU_FLAGS1_SIG_B_MCS_KNOWN', + 0x0040: 'IEEE80211_RADIOTAP_HE_MU_FLAGS1_SIG_B_DCM_KNOWN', + 0x0080: 'IEEE80211_RADIOTAP_HE_MU_FLAGS1_CH2_CTR_26T_RU_KNOWN', + 0x0100: 'IEEE80211_RADIOTAP_HE_MU_FLAGS1_CH1_RU_KNOWN', + 0x0200: 'IEEE80211_RADIOTAP_HE_MU_FLAGS1_CH2_RU_KNOWN', + 0x1000: 'IEEE80211_RADIOTAP_HE_MU_FLAGS1_CH1_CTR_26T_RU_KNOWN', + 0x4000: 'IEEE80211_RADIOTAP_HE_MU_FLAGS1_SIG_B_COMP_KNOWN', + 0x8000: 'IEEE80211_RADIOTAP_HE_MU_FLAGS1_SIG_B_SYMS_USERS_KNOWN', + } # type: dict[int, str] + + +if __name__ == '__main__': + sys.exit(HEMUFlags1Known()) # type: ignore[arg-type] diff --git a/pcapkit/vendor/radiotap/he_mu_flags2_known.py b/pcapkit/vendor/radiotap/he_mu_flags2_known.py new file mode 100644 index 0000000000..a20af4b753 --- /dev/null +++ b/pcapkit/vendor/radiotap/he_mu_flags2_known.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +"""Radiotap HE-MU Flags2 Known Flags +======================================= + +.. module:: pcapkit.vendor.radiotap.he_mu_flags2_known + +This module contains the vendor crawler for **Radiotap HE-MU Flags2 Known Flags**, +which is automatically generating :class:`pcapkit.const.radiotap.he_mu_flags2_known.HEMUFlags2Known`. + +The registry is the bits of the HE-MU field's ``flags2`` besides its value subfields, as radiotap.org +tables them; see :mod:`pcapkit.vendor.radiotap.flags` for how a row becomes a +member, and where its name comes from. + +""" + +import sys + +from pcapkit.vendor.radiotap.flags import Flags + +__all__ = ['HEMUFlags2Known'] + + +class HEMUFlags2Known(Flags): + """Radiotap HE-MU Flags2 Known Flags""" + + #: Value limit checker. + FLAG = 'isinstance(value, int) and 0 <= value <= 0xFFFF' + #: Link to registry. + LINK = 'https://www.radiotap.org/fields/HE-MU.html' + #: Subfield whose table is scraped. + SUBFIELD = 'flags2' + #: Masks of the value subfields, which the schema splits off. + VALUES = (0x0003, 0x0008, 0x00F0, 0x0300, 0x0800,) # type: tuple[int, ...] + #: The source macro of each bit, keyed by mask, which is Linux's. + NAMES = { + 0x0004: 'IEEE80211_RADIOTAP_HE_MU_FLAGS2_BW_FROM_SIG_A_BW_KNOWN', + 0x0400: 'IEEE80211_RADIOTAP_HE_MU_FLAGS2_PUNC_FROM_SIG_A_BW_KNOWN', + } # type: dict[int, str] + + +if __name__ == '__main__': + sys.exit(HEMUFlags2Known()) # type: ignore[arg-type] diff --git a/pcapkit/vendor/radiotap/usig_common_known.py b/pcapkit/vendor/radiotap/usig_common_known.py new file mode 100644 index 0000000000..e64752c700 --- /dev/null +++ b/pcapkit/vendor/radiotap/usig_common_known.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +"""Radiotap U-SIG Common Known Flags +======================================= + +.. module:: pcapkit.vendor.radiotap.usig_common_known + +This module contains the vendor crawler for **Radiotap U-SIG Common Known Flags**, +which is automatically generating :class:`pcapkit.const.radiotap.usig_common_known.USIGCommonKnown`. + +The registry is the bits of the U-SIG field's ``common`` besides its value subfields, as radiotap.org +tables them; see :mod:`pcapkit.vendor.radiotap.flags` for how a row becomes a +member, and where its name comes from. + +""" + +import sys + +from pcapkit.vendor.radiotap.flags import Flags + +__all__ = ['USIGCommonKnown'] + + +class USIGCommonKnown(Flags): + """Radiotap U-SIG Common Known Flags""" + + #: Value limit checker. + FLAG = 'isinstance(value, int) and 0 <= value <= 0xFFFFFFFF' + #: Link to registry. + LINK = 'https://www.radiotap.org/fields/U-SIG.html' + #: Subfield whose table is scraped. + SUBFIELD = 'common' + #: Masks of the value subfields, which the schema splits off. + VALUES = (0x00007000, 0x00038000, 0x00040000, 0x01F80000, 0xFE000000,) # type: tuple[int, ...] + #: The source macro of each bit, keyed by mask, which is Linux's. + NAMES = { + 0x00000001: 'IEEE80211_RADIOTAP_EHT_USIG_COMMON_PHY_VER_KNOWN', + 0x00000002: 'IEEE80211_RADIOTAP_EHT_USIG_COMMON_BW_KNOWN', + 0x00000004: 'IEEE80211_RADIOTAP_EHT_USIG_COMMON_UL_DL_KNOWN', + 0x00000008: 'IEEE80211_RADIOTAP_EHT_USIG_COMMON_BSS_COLOR_KNOWN', + 0x00000010: 'IEEE80211_RADIOTAP_EHT_USIG_COMMON_TXOP_KNOWN', + 0x00000020: 'IEEE80211_RADIOTAP_EHT_USIG_COMMON_BAD_USIG_CRC', + 0x00000040: 'IEEE80211_RADIOTAP_EHT_USIG_COMMON_VALIDATE_BITS_CHECKED', + 0x00000080: 'IEEE80211_RADIOTAP_EHT_USIG_COMMON_VALIDATE_BITS_OK', + } # type: dict[int, str] + + +if __name__ == '__main__': + sys.exit(USIGCommonKnown()) # type: ignore[arg-type] diff --git a/tests/const/census/radiotap.py b/tests/const/census/radiotap.py index 1aeb484d84..fc66782c1f 100644 --- a/tests/const/census/radiotap.py +++ b/tests/const/census/radiotap.py @@ -8,6 +8,10 @@ 'pcapkit.const.radiotap.eht_known:EHTKnown': 'flag', 'pcapkit.const.radiotap.field_type:FieldType': 'int', 'pcapkit.const.radiotap.flags:Flags': 'flag', + 'pcapkit.const.radiotap.he_data1_known:HEData1Known': 'flag', + 'pcapkit.const.radiotap.he_data2_known:HEData2Known': 'flag', + 'pcapkit.const.radiotap.he_mu_flags1_known:HEMUFlags1Known': 'flag', + 'pcapkit.const.radiotap.he_mu_flags2_known:HEMUFlags2Known': 'flag', 'pcapkit.const.radiotap.he_mu_other_user_known:HEMUOtherUserKnown': 'flag', 'pcapkit.const.radiotap.lsig_data1:LSIGData1': 'flag', 'pcapkit.const.radiotap.mcs_known:MCSKnown': 'flag', @@ -15,6 +19,7 @@ 'pcapkit.const.radiotap.s1g_known:S1GKnown': 'flag', 'pcapkit.const.radiotap.timestamp_flags:TimestampFlags': 'flag', 'pcapkit.const.radiotap.tx_flags:TXFlags': 'flag', + 'pcapkit.const.radiotap.usig_common_known:USIGCommonKnown': 'flag', 'pcapkit.const.radiotap.vht_coding:VHTCoding': 'flag', 'pcapkit.const.radiotap.vht_flags:VHTFlags': 'flag', 'pcapkit.const.radiotap.vht_known:VHTKnown': 'flag', diff --git a/tests/const/test_const_registry_protocol.py b/tests/const/test_const_registry_protocol.py index 29ad3c9af4..d440b14afa 100644 --- a/tests/const/test_const_registry_protocol.py +++ b/tests/const/test_const_registry_protocol.py @@ -78,6 +78,11 @@ ('pcapkit.const.radiotap.vht_known', 'VHTKnown', 'pcapkit/const/radiotap/vht_known.py'), ('pcapkit.const.radiotap.vht_flags', 'VHTFlags', 'pcapkit/const/radiotap/vht_flags.py'), ('pcapkit.const.radiotap.eht_known', 'EHTKnown', 'pcapkit/const/radiotap/eht_known.py'), + ('pcapkit.const.radiotap.he_data1_known', 'HEData1Known', 'pcapkit/const/radiotap/he_data1_known.py'), + ('pcapkit.const.radiotap.he_data2_known', 'HEData2Known', 'pcapkit/const/radiotap/he_data2_known.py'), + ('pcapkit.const.radiotap.he_mu_flags1_known', 'HEMUFlags1Known', 'pcapkit/const/radiotap/he_mu_flags1_known.py'), + ('pcapkit.const.radiotap.he_mu_flags2_known', 'HEMUFlags2Known', 'pcapkit/const/radiotap/he_mu_flags2_known.py'), + ('pcapkit.const.radiotap.usig_common_known', 'USIGCommonKnown', 'pcapkit/const/radiotap/usig_common_known.py'), ) #: The crawlers whose bespoke templates are converted, and which must therefore diff --git a/tests/corekit/test_field_roundtrip_unit.py b/tests/corekit/test_field_roundtrip_unit.py index 156e6ef060..bee384538d 100644 --- a/tests/corekit/test_field_roundtrip_unit.py +++ b/tests/corekit/test_field_roundtrip_unit.py @@ -119,6 +119,7 @@ def field_cases() -> 'tuple[FieldCase, ...]': # pylint: disable=too-many-locals """Every field case, built against the current :mod:`pcapkit` import.""" import aenum + from pcapkit.const.radiotap.he_data1_known import HEData1Known from pcapkit.const.reg.apptype import AppType, TransportProtocol from pcapkit.const.reg.transtype import TransType from pcapkit.corekit.fields import collections as fcoll @@ -189,6 +190,23 @@ class Aenum(aenum.IntEnum): f'PortEnumField/{proto.name}', lambda proto=proto: fnum.PortEnumField(length=2, namespace=AppType, proto=proto), (AppType.get(80, proto=proto),), _wires(2) + (b'\xd4\x31',))) + split = {'a': (0, 2), 'b': (2, 3)} + cases.append(FieldCase( + 'BitmapField/unnamed-rest', lambda: fnum.BitmapField(length=1, namespace=split), + ({'a': 0, 'b': 0}, {'a': 3, 'b': 7}, {'a': 1, 'b': 5, '__unnamed__': 0xe0}), + _wires(1) + (b'\xa5',))) + cases.append(FieldCase( + 'BitmapField/flags-rest', lambda: fnum.BitmapField( + length=2, namespace={'ppdu_format': (0, 2)}, remainder='known', flags=HEData1Known, + byteorder='little'), + ({'ppdu_format': 0, 'known': 0}, {'ppdu_format': 3, 'known': 0xfffc}, + {'ppdu_format': 2, 'known': HEData1Known.HE_DATA1_BSS_COLOR_KNOWN}), + _wires(2) + (b'\x02\x80',))) + cases.append(FieldCase( + 'BitmapField/int-rest', lambda: fnum.BitmapField( + length=4, namespace={'lo': (0, 4), 'mid': (12, 6)}, remainder='rest', + byteorder='little'), + ({'lo': 0, 'mid': 0, 'rest': 0}, {'lo': 15, 'mid': 63, 'rest': 0xfffc0ff0}), _wires(4))) # -- strings ------------------------------------------------------------ for width in (0, 1, 4): diff --git a/tests/corekit/test_fields_numbers_bitmap_2647_unit.py b/tests/corekit/test_fields_numbers_bitmap_2647_unit.py new file mode 100644 index 0000000000..7533cbdfdc --- /dev/null +++ b/tests/corekit/test_fields_numbers_bitmap_2647_unit.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +"""GitHub issue #2647: :class:`~pcapkit.corekit.fields.numbers.BitmapField`. + +A word split into subfields numbered from its least-significant bit, read in its +byte order first, so a subfield may cross an octet boundary of a little-endian +word. The bits no subfield names are kept in place, under a remainder, as a +member of a flag enumeration where one is given, or under ``__unnamed__``. + +Each test imports :mod:`pcapkit` itself, after +:func:`~tests._support.reimport_once_per_class`. + +""" + +import enum +import unittest + +from tests._support import reimport_once_per_class + +#: Test tier (#1958): hand-picked cases of one field type. +__tier__ = 'ALL' + + +class Known(enum.IntFlag): + """Stand-in known bits, of which bit 3 is left undefined.""" + + A = 0x0004 + B = 0x0010 + + +class TestBitmapField(unittest.TestCase): + """Pin the split, the remainder and the refusals.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def _schema(self, **kwargs): # type: ignore[no-untyped-def] + from pcapkit.corekit.fields.numbers import BitmapField, UInt8Field + from pcapkit.protocols.schema.schema import Schema, schema_final + + @schema_final + class Word(Schema): + word = BitmapField(**kwargs) + tail = UInt8Field() + + return Word + + def test_a_little_endian_word_splits_across_its_octets(self) -> None: + word = self._schema(length=4, byteorder='little', namespace={'low': (0, 3), 'mid': (12, 3), + 'cross': (19, 6), 'top': (25, 7)}) + raw = bytes.fromhex('0170f8ff') + b'\x2a' + parsed = word.unpack(raw, len(raw), None) + self.assertEqual(parsed.word, {'low': 1, 'mid': 7, 'cross': 0x3F, 'top': 0x7F}) + self.assertEqual(parsed.tail, 0x2A) + self.assertEqual(parsed.pack(), raw) + self.assertEqual(word.from_dict(parsed.to_dict()).pack(), raw) + # the bits under ``cross`` straddle the third and fourth octets + self.assertEqual(word(word={'low': 0, 'mid': 0, 'cross': 1, 'top': 0}, tail=0).pack(), + bytes.fromhex('00000800') + b'\x00') + + def test_the_byte_order_is_the_word_s(self) -> None: + word = self._schema(length=2, byteorder='big', namespace={'low': (0, 4)}) + parsed = word.unpack(bytes.fromhex('a5c3') + b'\x00', 3, None) + self.assertEqual(parsed.word, {'low': 3, '__unnamed__': 0xA5C0}) + self.assertEqual(parsed.pack(), bytes.fromhex('a5c300')) + + def test_the_remainder_is_a_flag_member_with_undefined_bits_kept(self) -> None: + word = self._schema(length=1, namespace={'format': (0, 2)}, remainder='known', flags=Known) + parsed = word.unpack(b'\x1e\x00', 2, None) + self.assertEqual(parsed.word['format'], 2) + self.assertIsInstance(parsed.word['known'], Known) + self.assertEqual(int(parsed.word['known']), 0x1C) + self.assertIn(Known.A, parsed.word['known']) + self.assertEqual(parsed.pack(), b'\x1e\x00') + # a plain int is taken for the remainder as well as a member + self.assertEqual(word(word={'format': 2, 'known': 0x1C}, tail=0).pack(), b'\x1e\x00') + + def test_a_remainder_without_flags_is_an_int(self) -> None: + word = self._schema(length=1, namespace={'format': (0, 2)}, remainder='known') + parsed = word.unpack(b'\x00\x00', 2, None) + self.assertEqual(parsed.word, {'format': 0, 'known': 0}) + self.assertIs(type(parsed.word['known']), int) + + def test_the_layout_is_checked_when_declared(self) -> None: + from pcapkit.corekit.fields.numbers import BitmapField + from pcapkit.utilities.exceptions import FieldValueError + + for namespace in ({'past': (6, 3)}, {'none': (0, 0)}, {'a': (0, 4), 'b': (3, 2)}): + with self.subTest(namespace=namespace), self.assertRaises(FieldValueError): + BitmapField(1, namespace) + with self.assertRaises(FieldValueError): + BitmapField(1, {'a': (0, 2)}, flags=Known) + field = BitmapField(2, {'a': (0, 2), 'b': (8, 6)}, remainder='rest') + self.assertEqual((field.subfields, field.remainder, field.remainder_mask, field.flags), + ({'a': (0, 2), 'b': (8, 6)}, 'rest', 0xC0FC, None)) + self.assertIs(BitmapField(1, {'a': (0, 2)}, remainder='known', flags=Known).flags, Known) + + def test_packing_refuses_what_the_bits_cannot_hold(self) -> None: + from pcapkit.utilities.exceptions import FieldValueError + + word = self._schema(length=1, namespace={'format': (0, 2)}, remainder='known', flags=Known) + for value in ({'format': 4, 'known': 0}, {'format': -1, 'known': 0}, {'format': True, 'known': 0}, + {'format': 0, 'known': 0x01}, {'format': 0, 'known': 'A'}): + with self.subTest(value=value), self.assertRaises(FieldValueError): + word(word=value, tail=0).pack() + + def test_packing_refuses_a_missing_subfield(self) -> None: + from pcapkit.utilities.exceptions import FieldValueError + + word = self._schema(length=1, namespace={'format': (0, 2), 'mode': (2, 1)}, + remainder='known') + for value in ({'mode': 0, 'known': 0}, {'format': 0, 'known': 0}, {}): + with self.subTest(value=value), self.assertRaisesRegex(FieldValueError, 'missing'): + word(word=value, tail=0).pack() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/corekit/test_schema_roundtrip_unit.py b/tests/corekit/test_schema_roundtrip_unit.py index 83efa425f3..f8f4b96d13 100644 --- a/tests/corekit/test_schema_roundtrip_unit.py +++ b/tests/corekit/test_schema_roundtrip_unit.py @@ -318,7 +318,7 @@ def minimal(field: 'Any', cls: 'type', depth: 'int', ctx: 'dict[str, Any]') -> ' from pcapkit.corekit.sentinels import NO_VALUE from pcapkit.corekit.fields.misc import (ConditionalField, ForwardMatchField, NoValueField, PayloadField, SchemaField, SwitchField) - from pcapkit.corekit.fields.numbers import EnumField, NumberField + from pcapkit.corekit.fields.numbers import BitmapField, EnumField, NumberField from pcapkit.corekit.fields.strings import BitField, BytesField, PaddingField, StringField if isinstance(field, (ConditionalField, ForwardMatchField)): @@ -334,6 +334,9 @@ def minimal(field: 'Any', cls: 'type', depth: 'int', ctx: 'dict[str, Any]') -> ' return field.default if hasattr(field, '_flags'): return {name: 0 for name in field._flags} # pylint: disable=protected-access + if isinstance(field, BitmapField): + return {**dict.fromkeys(field.subfields, 0), + **({} if field.remainder is None else {field.remainder: 0})} if isinstance(field, EnumField): namespace = field._namespace # pylint: disable=protected-access code, kind = own_code(cls), _code_type(cls) @@ -553,7 +556,7 @@ def patterned(cls: 'type', schema: 'Any', ctx: 'dict[str, Any]') -> 'Outcome': """ from pcapkit.corekit.fields import ipaddress as fip - from pcapkit.corekit.fields.numbers import EnumField, NumberField + from pcapkit.corekit.fields.numbers import BitmapField, EnumField, NumberField from pcapkit.corekit.fields.strings import BitField, BytesField, PaddingField names = _referenced(cls) @@ -564,7 +567,10 @@ def patterned(cls: 'type', schema: 'Any', ctx: 'dict[str, Any]') -> 'Outcome': continue if isinstance(field, EnumField) or hasattr(field, '_flags'): continue - if isinstance(field, NumberField): + if isinstance(field, BitmapField): + values[name] = {**dict.fromkeys(field.subfields, 1), + **({} if field.remainder is None else {field.remainder: 0})} + elif isinstance(field, NumberField): values[name] = 1 elif isinstance(field, BitField): values[name] = {sub: 1 for sub in field._namespace} # pylint: disable=protected-access diff --git a/tests/dumpkit/test_nameless_enum_rendering_unit.py b/tests/dumpkit/test_nameless_enum_rendering_unit.py index da031ecf00..f68e4bacd4 100644 --- a/tests/dumpkit/test_nameless_enum_rendering_unit.py +++ b/tests/dumpkit/test_nameless_enum_rendering_unit.py @@ -28,11 +28,14 @@ 'pcapkit.const.radiotap.ampdu_flags.AMPDUFlags': ('AMPDUStatus', 'flags'), 'pcapkit.const.radiotap.channel_flags.ChannelFlags': ('Channel', 'flags'), 'pcapkit.const.radiotap.eht_known.EHTKnown': ('EHT', 'known'), + 'pcapkit.const.radiotap.he_data2_known.HEData2Known': ('HE', 'data2'), + 'pcapkit.const.radiotap.he_mu_flags2_known.HEMUFlags2Known': ('HEMU', 'flags2'), 'pcapkit.const.radiotap.lsig_data1.LSIGData1': ('LSIG', 'data1'), 'pcapkit.const.radiotap.rx_flags.RXFlags': ('RXFlags', 'flags'), 'pcapkit.const.radiotap.s1g_known.S1GKnown': ('S1G', 'known'), 'pcapkit.const.radiotap.timestamp_flags.TimestampFlags': ('Timestamp', 'flags'), 'pcapkit.const.radiotap.tx_flags.TXFlags': ('TXFlags', 'flags'), + 'pcapkit.const.radiotap.usig_common_known.USIGCommonKnown': ('USIG', 'common'), 'pcapkit.const.radiotap.vht_coding.VHTCoding': ('VHT', 'coding'), 'pcapkit.const.radiotap.vht_flags.VHTFlags': ('VHT', 'flags'), 'pcapkit.const.radiotap.vht_known.VHTKnown': ('VHT', 'known'), diff --git a/tests/project/test_rfc_constants_one_home_1739_unit.py b/tests/project/test_rfc_constants_one_home_1739_unit.py index 23ec98c473..da8a13873a 100644 --- a/tests/project/test_rfc_constants_one_home_1739_unit.py +++ b/tests/project/test_rfc_constants_one_home_1739_unit.py @@ -337,6 +337,12 @@ "the 9-bit span bits 4-12 leave of RFC 2784's 12-bit Reserved0 once RFC 2890 " 'carves ``K``/``S`` out of it (:rfc:`2890#section-2`), not a PCAP-NG resolution-places ' 'table', + ('pcapkit/protocols/schema/link/radiotap.py', "{'bss_color': (19, 6)}"): + "the 6-bit BSS color at bit 19 of the U-SIG field's common word, not the IPv6 Next Header offset", + ('pcapkit/protocols/schema/link/radiotap.py', "{'phy_version': (12, 3)}"): + "the PHY version at bit 12 of the U-SIG field's common word, not the TCP Data Offset octet", + ('pcapkit/protocols/schema/link/radiotap.py', "{'ru_offset': (8, 6)}"): + "the 6-bit RU allocation offset of the HE field's data2, not the IPv6 Next Header offset", ('pcapkit/protocols/schema/misc/pcapng.py', 'BytesField(length=16)'): 'an IPv6 address is 16 octets', ('pcapkit/protocols/schema/misc/pcapng.py', 'BytesField(length=6)'): diff --git a/tests/protocols/link/test_radiotap_fields_2576_unit.py b/tests/protocols/link/test_radiotap_fields_2576_unit.py index c198c354d1..8f591cb296 100644 --- a/tests/protocols/link/test_radiotap_fields_2576_unit.py +++ b/tests/protocols/link/test_radiotap_fields_2576_unit.py @@ -72,6 +72,13 @@ NO_LAYOUT = 'no layout is defined for the field type' PAST = 'the field runs past the field-data region' +#: The MCS ``flags`` the vectors carry, split into their value subfields (#2647). +MCS_FLAGS = { + 0x25: {'bandwidth': 1, 'guard_interval': 1, 'ht_format': 0, 'fec_type': 0, 'stbc_streams': 1, 'ness_bit0': 0}, + 0x41: {'bandwidth': 1, 'guard_interval': 0, 'ht_format': 0, 'fec_type': 0, 'stbc_streams': 2, 'ness_bit0': 0}, + 0x82: {'bandwidth': 2, 'guard_interval': 0, 'ht_format': 0, 'fec_type': 0, 'stbc_streams': 0, 'ness_bit0': 1}, +} + #: Wire octets of each field below, from 0x81 up, so that a signed octet is negative. PATTERN = bytes(range(0x81, 0xA1)) @@ -95,16 +102,20 @@ 'TX_flags': (1, {'flags': 0x8281}), 'RTS_retries': (0, {'retries': 0x81}), 'data_retries': (0, {'retries': 0x81}), - 'MCS': (0, {'known': 0x81, 'flags': 0x82, 'mcs': 0x83}), + 'MCS': (0, {'known': 0x81, 'flags': MCS_FLAGS[0x82], 'mcs': 0x83}), 'A_MPDU_status': (3, {'reference': 0x84838281, 'flags': 0x8685, 'delimiter_crc': 0x87, 'reserved': 0x88}), 'VHT': (1, {'known': 0x8281, 'flags': 0x83, 'bandwidth': 0x84, 'mcs_nss': (0x85, 0x86, 0x87, 0x88), 'coding': 0x89, 'group_id': 0x8A, 'partial_aid': 0x8C8B}), 'timestamp': (7, {'timestamp': 0x8887868584838281, 'accuracy': 0x8A89, 'unit_position': 0x8B, 'flags': 0x8C}), - 'HE': (1, {'data1': 0x8281, 'data2': 0x8483, 'data3': 0x8685, 'data4': 0x8887, 'data5': 0x8A89, + 'HE': (1, {'data1': {'ppdu_format': 1, 'known': 0x8280}, + 'data2': {'ru_offset': 0x04, 'pri_sec_80mhz': 1, 'known': 0x0083}, 'data3': 0x8685, 'data4': 0x8887, 'data5': 0x8A89, 'data6': 0x8C8B}), - 'HE_MU': (1, {'flags1': 0x8281, 'flags2': 0x8483, 'ru_channel1': (0x85, 0x86, 0x87, 0x88), + 'HE_MU': (1, {'flags1': {'sig_b_mcs': 1, 'sig_b_dcm': 0, 'ch1_center_26_tone_ru': 0, 'known': 0x8280}, + 'flags2': {'bandwidth': 3, 'sig_b_compression': 0, 'sig_b_symbols_users': 8, + 'preamble_puncturing': 0, 'ch2_center_26_tone_ru': 0, 'known': 0x8400}, + 'ru_channel1': (0x85, 0x86, 0x87, 0x88), 'ru_channel2': (0x89, 0x8A, 0x8B, 0x8C)}), 'HE_MU_other_user': (1, {'per_user_1': 0x8281, 'per_user_2': 0x8483, 'per_user_position': 0x85, 'per_user_known': 0x86}), @@ -263,17 +274,18 @@ def test_tcpdump_captures(self) -> None: 'rx-stbc#1': [('TSFT', {'mactime': 7268}), ('Flags', {'flags': 0x10}), ('Channel', {'frequency': 2462, 'flags': 0x0480}), (signal, {'signal': -51}), ('Antenna', {'antenna': 1}), ('RX_flags', {'flags': 0}), - ('MCS', {'known': 0x27, 'flags': 0x25, 'mcs': 7})], + ('MCS', {'known': 0x27, 'flags': MCS_FLAGS[0x25], 'mcs': 7})], # 119738173us tsft 2462 MHz 11n -46dBm signal antenna 1 ... MCS 7 40 MHz long GI RX-STBC2 'rx-stbc#2': [('TSFT', {'mactime': 119738173}), ('Flags', {'flags': 0x10}), ('Channel', {'frequency': 2462, 'flags': 0x0480}), (signal, {'signal': -46}), ('Antenna', {'antenna': 1}), ('RX_flags', {'flags': 0}), - ('MCS', {'known': 0x27, 'flags': 0x41, 'mcs': 7})], + ('MCS', {'known': 0x27, 'flags': MCS_FLAGS[0x41], 'mcs': 7})], # 967750278us tsft wep 5180 MHz 11a -45dBm signal -107dBm noise antenna 0 [bit 23] 'htc#1': [('TSFT', {'mactime': 967750278}), ('Flags', {'flags': 0x04}), ('Channel', {'frequency': 5180, 'flags': 0x0140}), (signal, {'signal': -45}), ('Antenna_noise', {'noise': -107}), ('Antenna', {'antenna': 0}), - ('HE', {'data1': 0xC3FC, 'data2': 0x00FE, 'data3': 0x69E5, 'data4': 0x000F, + ('HE', {'data1': {'ppdu_format': 0, 'known': 0xC3FC}, + 'data2': {'ru_offset': 0, 'pri_sec_80mhz': 0, 'known': 0x00FE}, 'data3': 0x69E5, 'data4': 0x000F, 'data5': 0x2180, 'data6': 0x7F02}), ('Vendor_Namespace', {'oui': b'\x00\x03\x7f', 'sub_namespace': 0, 'skip_length': 16, 'data': bytes.fromhex('cb050204feff000000000000e06e8e27')})], @@ -335,7 +347,7 @@ def test_the_unofficial_retries_fields(self) -> None: parsed = self._parse(raw) self.assertEqual(self._entries(parsed), [ ('TX_flags', {'flags': 0x0002}), ('RTS_retries', {'retries': 3}), ('data_retries', {'retries': 1}), - ('MCS', {'known': 0x27, 'flags': 0x25, 'mcs': 7}), + ('MCS', {'known': 0x27, 'flags': MCS_FLAGS[0x25], 'mcs': 7}), ]) self._assert_round_trips(parsed, raw) @@ -370,7 +382,9 @@ def item(tlv_type: int, data: bytes) -> bytes: got = [(item.type.name, item.length, item.data) for item in entries[1][1]['tlvs']] self.assertEqual(got[:3], [ ('S1G', 6, {'type': 32, 'known': 1, 'data1': 2, 'data2': 3}), - ('U_SIG', 12, {'type': 33, 'common': 0x11, 'value': 0x22, 'mask': 0x33}), + ('U_SIG', 12, {'type': 33, 'common': {'phy_version': 0, 'bandwidth': 0, 'ul_dl': 0, 'bss_color': 0, + 'txop': 0, 'known': 0x11}, + 'value': 0x22, 'mask': 0x33}), ('EHT', 48, {'type': 34, 'known': 0xE0, 'data': tuple(range(1, 10)), 'user_info': (0xA1, 0xA2)}), ]) self.assertEqual(got[3], ('Vendor_Namespace', 10, {'oui': bytes.fromhex('001374'), 'subtype': 1, @@ -436,8 +450,8 @@ def test_bitmaps_decode_into_their_flag_registries(self) -> None: self._assert_flag(decoded[21].coding, VHTCoding, 0x15, VHTCoding.CODING_LDPC_USER0 | VHTCoding.CODING_LDPC_USER2) self.assertEqual(int(decoded[3].flags) & ~0x00A0, 0x8008) - # the MCS flags hold value subfields, so they stay a plain integer - self.assertIs(type(decoded[19].flags), int) + # the MCS flags hold value subfields alone, which split off (#2647) + self.assertEqual(decoded[19].flags, MCS_FLAGS[0x25]) self._assert_round_trips(parsed, raw) # A-MPDU status, timestamp, HE-MU-other-user and L-SIG; the reserved @@ -691,7 +705,7 @@ def constructor(field_type, entry): # type: ignore[no-untyped-def] self.assertEqual(self._entries(parsed), [ ('Flags', {'flags': 0x10}), ('Unassigned', {'flags': 0x0140, 'frequency': 2412, 'channel': 1, 'maxpower': 20}), - ('MCS', {'known': 0x27, 'flags': 0x25, 'mcs': 7}), + ('MCS', {'known': 0x27, 'flags': MCS_FLAGS[0x25], 'mcs': 7}), ]) self.assertIsInstance(parsed.info.decoded[18], XChannel) self._assert_round_trips(parsed, raw) diff --git a/tests/protocols/link/test_radiotap_mixed_bitmaps_2647_unit.py b/tests/protocols/link/test_radiotap_mixed_bitmaps_2647_unit.py new file mode 100644 index 0000000000..67ae703c6b --- /dev/null +++ b/tests/protocols/link/test_radiotap_mixed_bitmaps_2647_unit.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- +"""GitHub issue #2647: radiotap's mixed bitmaps, split into value subfields and known bits. + +The MCS ``flags``, the HE ``data1`` and ``data2``, the HE-MU ``flags1`` and +``flags2`` and the U-SIG ``common`` mix value subfields with known bits. Each +decodes into its value subfields, integers, and the rest of its bits, a member of +its known-bits registry that keeps the undefined ones, so both round trips stay +byte-exact. The MCS ``flags`` holds value subfields alone, its known bits being +the MCS field's ``known``. + +The subfield masks are checked against the Linux kernel's +``include/net/ieee80211_radiotap.h`` at ``e5fefa3da5bf``, Wireshark's +``packet-ieee80211-radiotap-defs.h`` at ``090e75fa5daa`` and radiotap.org's field +pages, as the radiotap.org rows the crawlers leave out. + +Each test imports :mod:`pcapkit` itself, after +:func:`~tests._support.reimport_once_per_class`. + +""" + +import struct +import unittest + +from tests._support import reimport_once_per_class + +#: Each value subfield's mask, by ``(schema, field)``, in the Linux kernel and in +#: Wireshark. Linux names no macro for bit 7 of the MCS ``flags``. +LINUX = { + ('MCS', 'flags'): {'bandwidth': 0x03, 'guard_interval': 0x04, 'ht_format': 0x08, 'fec_type': 0x10, + 'stbc_streams': 0x60}, + ('HE', 'data1'): {'ppdu_format': 0x0003}, + ('HE', 'data2'): {'ru_offset': 0x3F00, 'pri_sec_80mhz': 0x8000}, + ('HEMU', 'flags1'): {'sig_b_mcs': 0x000F, 'sig_b_dcm': 0x0020, 'ch1_center_26_tone_ru': 0x2000}, + ('HEMU', 'flags2'): {'bandwidth': 0x0003, 'sig_b_compression': 0x0008, 'sig_b_symbols_users': 0x00F0, + 'preamble_puncturing': 0x0300, 'ch2_center_26_tone_ru': 0x0800}, + ('USIG', 'common'): {'phy_version': 0x00007000, 'bandwidth': 0x00038000, 'ul_dl': 0x00040000, + 'bss_color': 0x01F80000, 'txop': 0xFE000000}, +} +WIRESHARK = {**LINUX, ('MCS', 'flags'): {**LINUX[('MCS', 'flags')], 'ness_bit0': 0x80}} + +#: The masks radiotap.org's MCS page tabulates for ``flags``, all value subfields. +MCS_FLAGS_ROWS = (0x03, 0x04, 0x08, 0x10, 0x60, 0x80) + +#: The mixed bitmaps with known bits, to their known-bits crawler module and class. +CRAWLERS = { + ('HE', 'data1'): ('he_data1_known', 'HEData1Known'), + ('HE', 'data2'): ('he_data2_known', 'HEData2Known'), + ('HEMU', 'flags1'): ('he_mu_flags1_known', 'HEMUFlags1Known'), + ('HEMU', 'flags2'): ('he_mu_flags2_known', 'HEMUFlags2Known'), + ('USIG', 'common'): ('usig_common_known', 'USIGCommonKnown'), +} + + +def _radiotap(present: 'list[int]', fields: bytes = b'') -> bytes: + """Pack a radiotap header: fixed start, present-flags chain, fields.""" + return struct.pack(' 'dict[str, int]': # type: ignore[no-untyped-def] + """Each subfield of a bitmap field, as a mask.""" + return {name: ((1 << size) - 1) << start for name, (start, size) in field.subfields.items()} + + +class TestRadiotapMixedBitmaps(unittest.TestCase): + """Pin the split of the mixed bitmaps, and its round trips.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def _parse(self, raw: bytes): # type: ignore[no-untyped-def] + from pcapkit.protocols.link.radiotap import Radiotap + + parsed = Radiotap(raw, len(raw)) + self.assertEqual(parsed.data, raw) + for data in (parsed.info, parsed.info.to_dict()): + self.assertEqual(Radiotap.from_data(data).data, raw) + self.assertEqual(Radiotap(present=parsed.info.present, fields=parsed.info.decoded).data, raw) + return parsed + + def test_the_layouts_match_linux_wireshark_and_radiotap_org(self) -> None: + import importlib + + from pcapkit.protocols.schema.link import radiotap + + for source, table in (('linux', LINUX), ('wireshark', WIRESHARK)): + for (schema, name), masks in table.items(): + with self.subTest(source=source, field=f'{schema}.{name}'): + field = getattr(radiotap, schema).__fields__[name] + self.assertEqual({sub: mask for sub, mask in _masks(field).items() if sub in masks}, masks) + self.assertEqual(set(_masks(field)) - set(masks), {'ness_bit0'} if source == 'linux' + and schema == 'MCS' else set()) + self.assertEqual(sorted(_masks(radiotap.MCS.__fields__['flags']).values()), list(MCS_FLAGS_ROWS)) + for (schema, name), (module, klass) in CRAWLERS.items(): + with self.subTest(source='radiotap.org', field=f'{schema}.{name}'): + field = getattr(radiotap, schema).__fields__[name] + crawler = getattr(importlib.import_module(f'pcapkit.vendor.radiotap.{module}'), klass) + registry = getattr(importlib.import_module(f'pcapkit.const.radiotap.{module}'), klass) + self.assertEqual(sorted(_masks(field).values()), sorted(crawler.VALUES)) + self.assertIs(field.flags, registry) + self.assertEqual(field.remainder, 'known') + for member in registry: + self.assertTrue(int(member) & field.remainder_mask, member) + + def test_each_bitmap_splits_and_keeps_its_undefined_bits(self) -> None: + """Every bit set, reserved ones included; and every bit clear.""" + from pcapkit.const.radiotap.he_data1_known import HEData1Known + from pcapkit.const.radiotap.he_data2_known import HEData2Known + from pcapkit.const.radiotap.he_mu_flags1_known import HEMUFlags1Known + from pcapkit.const.radiotap.he_mu_flags2_known import HEMUFlags2Known + from pcapkit.const.radiotap.usig_common_known import USIGCommonKnown + + for fill in (0xFF, 0x00): + with self.subTest(fill=hex(fill)): + # MCS, HE (after a pad octet) and HE-MU, then U-SIG as a TLV item + fields = (bytes([0x01, fill, 0x07]) + b'\x00' + bytes([fill] * 4) + bytes(8) + + bytes([fill] * 4) + bytes(8)) + parsed = self._parse(_radiotap([1 << 19 | 1 << 23 | 1 << 24], fields)) + decoded = parsed.info.decoded + ones = 1 if fill else 0 + self.assertEqual(decoded[19].flags, {'bandwidth': 3 * ones, 'guard_interval': ones, 'ht_format': ones, + 'fec_type': ones, 'stbc_streams': 3 * ones, 'ness_bit0': ones}) + self.assertEqual(decoded[23].data1, {'ppdu_format': 3 * ones, 'known': 0xFFFC * ones}) + self.assertEqual(decoded[23].data2, {'ru_offset': 0x3F * ones, 'pri_sec_80mhz': ones, + 'known': 0x40FF * ones}) + self.assertEqual(decoded[24].flags1, {'sig_b_mcs': 0xF * ones, 'sig_b_dcm': ones, + 'ch1_center_26_tone_ru': ones, 'known': 0xDFD0 * ones}) + self.assertEqual(decoded[24].flags2, {'bandwidth': 3 * ones, 'sig_b_compression': ones, + 'sig_b_symbols_users': 0xF * ones, 'preamble_puncturing': 3 * ones, + 'ch2_center_26_tone_ru': ones, 'known': 0xF404 * ones}) + for value, registry in ((decoded[23].data1['known'], HEData1Known), + (decoded[23].data2['known'], HEData2Known), + (decoded[24].flags1['known'], HEMUFlags1Known), + (decoded[24].flags2['known'], HEMUFlags2Known)): + self.assertIsInstance(value, registry) + + tlv = struct.pack(' None: + from pcapkit.const.radiotap.he_mu_flags1_known import HEMUFlags1Known + + parsed = self._parse(_radiotap([1 << 24], struct.pack(' None: + from pcapkit.const.radiotap.he_data1_known import HEData1Known + from pcapkit.protocols.link.radiotap import Radiotap + + expected = _radiotap([1 << 23], struct.pack('<6H', 0x0006, 0, 0, 0, 0, 0)) + rest = {'data2': {'ru_offset': 0, 'pri_sec_80mhz': 0, 'known': 0}, + 'data3': 0, 'data4': 0, 'data5': 0, 'data6': 0} + for known in (HEData1Known.HE_DATA1_BSS_COLOR_KNOWN, HEData1Known.get(0x0004), 0x0004): + with self.subTest(known=known): + made = Radiotap(present=(1 << 23,), fields=[(23, {'data1': {'ppdu_format': 2, 'known': known}, + **rest})]) + self.assertEqual(made.data, expected) + + def test_make_refuses_what_a_bitmap_cannot_hold(self) -> None: + from pcapkit.protocols.link.radiotap import Radiotap + from pcapkit.utilities.exceptions import ProtocolError + + flags = {'bandwidth': 0, 'guard_interval': 0, 'ht_format': 0, 'fec_type': 0, 'stbc_streams': 0, 'ness_bit0': 0} + cases = { + 'an int for the bitmap': {**flags, 'flags': 0x25}, + 'a missing subfield': {'flags': {key: value for key, value in flags.items() if key != 'ness_bit0'}}, + 'a subfield past its bits': {'flags': {**flags, 'stbc_streams': 4}}, + 'a bool': {'flags': {**flags, 'bandwidth': True}}, + 'a negative': {'flags': {**flags, 'bandwidth': -1}}, + } + for name, entry in cases.items(): + with self.subTest(case=name), self.assertRaises(ProtocolError): + Radiotap(present=(1 << 19,), fields=[(19, {'known': 0, 'mcs': 0, **entry})]) + common = {'phy_version': 0, 'bandwidth': 0, 'ul_dl': 0, 'bss_color': 0, 'txop': 0} + for name, known in (('known bits of a value', 0x1000), ('known bits past the word', 1 << 32), + ('a missing known', None)): + with self.subTest(case=name), self.assertRaises(ProtocolError): + value = {**common} if known is None else {**common, 'known': known} + Radiotap(present=(1 << 28,), fields=[(28, {'tlvs': [ + {'type': 33, 'data': {'common': value, 'value': 0, 'mask': 0}}]})]) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/protocols/link/test_radiotap_short_tlv_2655_unit.py b/tests/protocols/link/test_radiotap_short_tlv_2655_unit.py index 33077a9484..6e1b0d7b64 100644 --- a/tests/protocols/link/test_radiotap_short_tlv_2655_unit.py +++ b/tests/protocols/link/test_radiotap_short_tlv_2655_unit.py @@ -46,14 +46,19 @@ 'vendor': (30, bytes.fromhex('001374') + struct.pack(' None: from pcapkit.protocols.link.radiotap import Radiotap from pcapkit.utilities.exceptions import ProtocolError - usig = {'common': 1, 'value': 2, 'mask': 3} + usig = {'common': COMMON, 'value': 2, 'mask': 3} + # whole, the item builds, so each refusal below is its record's + Radiotap(present=(PRESENT,), fields=[(28, {'tlvs': [{'type': 33, 'data': usig}]})]) for record in (('nosuch', 0), ('mask', -1), ('mask', True), ('mask', '0'), ('mask',), 'mask'): with self.subTest(record=record): with self.assertRaises(ProtocolError): diff --git a/tests/vendor/test_radiotap_crawler_2647_unit.py b/tests/vendor/test_radiotap_crawler_2647_unit.py new file mode 100644 index 0000000000..ade87944d4 --- /dev/null +++ b/tests/vendor/test_radiotap_crawler_2647_unit.py @@ -0,0 +1,235 @@ +# -*- coding: utf-8 -*- +"""GitHub issue #2647: the crawlers of radiotap's mixed bitmaps. + +The HE field's ``data1`` and ``data2``, the HE-MU field's ``flags1`` and +``flags2`` and the U-SIG field's ``common`` mix value subfields with known bits. +Each has a registry of its known bits, whose crawler scrapes the subfield's table +on its field page as the #2576 flag crawlers do, less the rows of its ``VALUES``. +Regenerating each constant module from the fixtures below, the tables as +radiotap.org served them on 2026-11-02, is a no-op against the committed file. + +**The fixtures are built inline and parsed with the same** ``html5lib`` **the +crawlers use**, rather than fetched, as in ``test_radiotap_crawler_2576_unit.py``. +Each page keeps the tables its crawlers read and the one after them, which none +takes; the HE page's RU allocation offset table, under ``data2`` too, is cut to +its first rows, none of which has a mask. + +The suite is unit-tier (see :mod:`tests._tiers`): it reads no capture and makes +no network call. + +""" +from __future__ import annotations + +import html +import importlib +import importlib.util +import pathlib +import unittest +from typing import TYPE_CHECKING + +from tests._support import reimport_once_per_class + +if TYPE_CHECKING: + from typing import Any + +#: Test tier (#1958): renders the constant modules from recorded data and compares them +#: with the committed files, which the vendor scripts regenerate on one interpreter. +__tier__ = 'LATEST' + +#: Repository root, i.e. the grandparent of the directory holding this file. +ROOT = pathlib.Path(__file__).resolve().parents[2] + +#: The distributions importing :mod:`pcapkit.vendor` with its crawlers needs. +VENDOR_DEPS = ('requests', 'bs4', 'html5lib') + +#: Whether the crawlers are importable at all. +HAS_VENDOR_DEPS = all(importlib.util.find_spec(name) is not None for name in VENDOR_DEPS) + +#: Each crawler, as ``(module, class, page, subfield, value masks)``. +CRAWLERS = ( + ('he_data1_known', 'HEData1Known', 'HE', 'data1', (0x0003,)), + ('he_data2_known', 'HEData2Known', 'HE', 'data2', (0x3F00, 0x8000)), + ('he_mu_flags1_known', 'HEMUFlags1Known', 'HE-MU', 'flags1', (0x000F, 0x0020, 0x2000)), + ('he_mu_flags2_known', 'HEMUFlags2Known', 'HE-MU', 'flags2', (0x0003, 0x0008, 0x00F0, 0x0300, 0x0800)), + ('usig_common_known', 'USIGCommonKnown', 'U-SIG', 'common', + (0x00007000, 0x00038000, 0x00040000, 0x01F80000, 0xFE000000)), +) + +#: Prefixes the name maps' macros carry, which the member names drop. +PREFIXES = ('IEEE80211_RADIOTAP_', 'IEEE80211_') + +#: The tables of each page, as ``(heading, paragraph, rows)``: the ``

`` before +#: the table, if any, and the paragraph between them, if any. +PAGES = { + 'HE': ( + ('data1', '', ( + ('0x0003', 'HE PPDU Format: 0=HE_SU, 1=HE_EXT_SU, 2=HE_MU, 3=HE_TRIG'), + ('0x0004', 'BSS Color known'), ('0x0008', 'Beam Change known'), ('0x0010', 'UL/DL known'), + ('0x0020', 'data MCS known'), ('0x0040', 'data DCM known'), ('0x0080', 'Coding known'), + ('0x0100', 'LDPC extra symbol segment known'), ('0x0200', 'STBC known'), + ('0x0400', 'Spatial Reuse known (Spatial Reuse 1 for HE_TRIG format)'), + ('0x0800', 'Spatial Reuse 2 known (HE_TRIG format), STA-ID known (HE_MU format)'), + ('0x1000', 'Spatial Reuse 3 known (HE_TRIG format)'), ('0x2000', 'Spatial Reuse 4 known (HE_TRIG format)'), + ('0x4000', 'data BW/RU allocation known'), ('0x8000', 'Doppler known'), + )), + ('data2', '', ( + ('0x0001', 'pri/sec 80 MHz known'), ('0x0002', 'GI known'), ('0x0004', 'number of LTF symbols known'), + ('0x0008', 'Pre-FEC Padding Factor known'), ('0x0010', 'TxBF known'), ('0x0020', 'PE Disambiguity known'), + ('0x0040', 'TXOP known'), ('0x0080', 'midamble periodicity known'), ('0x3f00', 'RU allocation offset'), + ('0x4000', 'RU allocation offset known'), ('0x8000', 'pri/sec 80 MHz (primary=0, secondary=1)'), + )), + (None, 'The RU allocation offset is encoded as the offset (within the 80 MHz pri/sec channel) of the ' + 'location in the correct tone column.', ( + ('26-tone', '52-tone', '106-tone', '242-tone', '484-tone', '996-tone', '2x996-tone'), + ('0', '0', '0', '0', '0', '0', '0'), ('1',), ('2', '1'), + )), + ('data3', '', ( + ('0x003f', 'BSS Color'), ('0x0040', 'Beam Change'), ('0x0080', 'UL/DL'), + )), + ), + 'HE-MU': ( + ('flags1', '', ( + ('0x000f', 'SIG-B MCS (from SIG-A)'), ('0x0010', 'SIG-B MCS known'), + ('0x0020', 'SIG-B DCM (from SIG-A)'), ('0x0040', 'SIG-B DCM known'), + ('0x0080', '(Channel 2) Center 26-tone RU bit known'), + ('0x0100', 'Channel 1 RUs known (which depends on BW)'), + ('0x0200', 'Channel 2 RUs known (which depends on BW)'), ('0x0c00', '(reserved)'), + ('0x1000', '(Channel 1) Center 26-tone RU bit known'), ('0x2000', '(Channel 1) Center 26-tone RU value'), + ('0x4000', 'SIG-B Compression known'), ('0x8000', '# of HE-SIG-B Symbols/MU-MIMO Users known'), + )), + ('flags2', '', ( + ('0x0003', 'bandwidth from Bandwidth field in HE-SIG-A (0 - 20 MHz, …, 3 - 160/80+80 MHz)'), + ('0x0004', 'bandwidth from Bandwidth field in HE-SIG-A known'), + ('0x0008', 'SIG-B compression from SIG-A'), + ('0x00f0', '# of HE-SIG-B Symbols - 1 or # of MU-MIMO Users - 1 from SIG-A'), + ('0x0300', 'preamble puncturing from Bandwidth field in HE-SIG-A (0 - non-puncturing, 1 - punctured ' + 'secondary 20 MHz (in primary 80 MHz if applicable), 2 - punctured but primary 40 MHz is ' + 'present (in primary 80 MHz if applicable)'), + ('0x0400', 'preamble puncturing from Bandwidth field in HE-SIG-A known'), + ('0x0800', '(Channel 2) Center 26-tone RU value'), ('0xf000', '(reserved)'), + )), + ), + 'U-SIG': ( + ('common', '', ( + ('0x00000001', 'PHY version identifier known'), ('0x00000002', 'BW known'), + ('0x00000004', 'UL/DL known'), ('0x00000008', 'BSS Color known'), ('0x00000010', 'TXOP known'), + ('0x00000020', 'bad U-SIG CRC'), ('0x00000040', 'validate bits checked'), + ('0x00000080', 'validate bits OK'), ('0x00000f00', '(reserved)'), + ('0x00007000', 'PHY version identifier'), ('0x00038000', 'BW'), ('0x00040000', 'UL/DL'), + ('0x01f80000', 'BSS Color'), ('0xfe000000', 'TXOP'), + )), + ('value / mask', 'An EHT PPDU is an EHT MU PPDU if', ( + ('bits', 'U-SIG reference', 'Content'), + ('0x0000001f', 'U-SIG-1 B20-B24', 'Disregard (all ones)'), + ('0x00000020', 'U-SIG-1 B25', 'Validate (must be 1)'), + )), + ), +} + + +def _page(name: 'str') -> 'str': + """Render the fixture of a field page, in the shape the live one has.""" + parts = [f'Radiotap - {name}

{name}

'] + for heading, paragraph, rows in PAGES[name]: + if heading is not None: + parts.append(f'

{heading}

') + if paragraph: + parts.append(f'

{html.escape(paragraph)}

') + parts.append('') + parts.extend('' + ''.join(f'' for cell in row) + '' for row in rows) + parts.append('
{html.escape(cell)}
') + parts.append('') + return ''.join(parts) + + +def _normalize(context: 'str') -> 'str': + """Apply the whitespace normalisation the generator writes files through.""" + lines = [] # type: list[str] + for line in context.splitlines(): + if line: + if line.strip(): + lines.append(line.rstrip()) + else: + lines.append(line) + return '\n'.join(lines) + '\n' + + +@unittest.skipUnless(HAS_VENDOR_DEPS, f'vendor extra not installed ({", ".join(VENDOR_DEPS)})') +class RadiotapMixedBitmapCrawlerTests(unittest.TestCase): + """The known-bit registries of the mixed bitmaps, and their crawlers.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def _crawler(self, module: 'str', name: 'str') -> 'Any': + """The crawler class, skipping when it was imported from another tree.""" + vendor_module = importlib.import_module(f'pcapkit.vendor.radiotap.{module}') + const_module = importlib.import_module(f'pcapkit.const.radiotap.{module}') + for found in (vendor_module, const_module): + resolved = pathlib.Path(found.__file__).resolve() + if ROOT not in resolved.parents: + self.skipTest(f'{found.__name__} was imported from {resolved}, which is outside ' + f'{ROOT}; install this checkout with `pip install -e .`') + return getattr(vendor_module, name) + + def test_the_registries_regenerate(self) -> None: + """Rendering each page through its crawler reproduces the committed module.""" + for module, name, page, _, _ in CRAWLERS: + with self.subTest(crawler=name): + klass = self._crawler(module, name) + vendor = klass.__new__(klass) + vendor.NAME = klass.__name__ + vendor.DOCS = klass.__doc__ + data = vendor.request(_page(page)) + vendor.record = vendor.count(data) + path = ROOT / 'pcapkit' / 'const' / 'radiotap' / f'{module}.py' + self.assertEqual(_normalize(vendor.context(data)), path.read_text(encoding='utf-8'), + f'regenerating {path.relative_to(ROOT)} would change it; run ' + '`pcapkit-vendor radiotap` and commit the result') + + def test_each_crawler_scrapes_its_subfield(self) -> None: + """Each crawler names its page and subfield, and takes that subfield's tables alone.""" + for module, name, page, subfield, _ in CRAWLERS: + with self.subTest(crawler=name): + klass = self._crawler(module, name) + self.assertEqual((klass.LINK, klass.SUBFIELD), + (f'https://www.radiotap.org/fields/{page}.html', subfield)) + tables = klass.__new__(klass).request(_page(page)) + # data2's RU allocation offset table is under its heading too + self.assertEqual(len(tables), 2 if subfield == 'data2' else 1) + + def test_the_value_rows_are_no_members(self) -> None: + """Every bit but the value subfields' and the reserved ones is a member, under Linux's name.""" + for module, name, page, _, values in CRAWLERS: + with self.subTest(crawler=name): + klass = self._crawler(module, name) + self.assertEqual(klass.VALUES, values) + registry = getattr(importlib.import_module(f'pcapkit.const.radiotap.{module}'), name) + members = {int(member): member.name for member in registry} + rows = {int(row[0], 16) for heading, _, table in PAGES[page] if heading == klass.SUBFIELD + for row in table if row[0].startswith('0x')} + singles = {mask for mask in rows if not mask & (mask - 1)} + self.assertEqual(set(members), singles - set(values)) + self.assertEqual(set(klass.NAMES), set(members), 'a member falls back to radiotap.org wording') + for mask, macro in klass.NAMES.items(): + prefix = next(prefix for prefix in PREFIXES if macro.startswith(prefix)) + self.assertEqual(members[mask], macro[len(prefix):]) + + def test_the_registries_keep_the_bits_they_do_not_name(self) -> None: + """A reserved bit stays in the value; a value past the word is refused.""" + from pcapkit.const.radiotap.he_mu_flags1_known import HEMUFlags1Known + from pcapkit.const.radiotap.usig_common_known import USIGCommonKnown + + member = HEMUFlags1Known.get(0x0C10) + self.assertEqual(int(member), 0x0C10) + self.assertIn(HEMUFlags1Known.HE_MU_FLAGS1_SIG_B_MCS_KNOWN, member) + member = USIGCommonKnown.get(0x0F21) + self.assertEqual(int(member), 0x0F21) + self.assertIn(USIGCommonKnown.EHT_USIG_COMMON_BAD_USIG_CRC, member) + for registry, value in ((HEMUFlags1Known, 0x10000), (USIGCommonKnown, 1 << 32)): + with self.subTest(registry=registry.__name__), self.assertRaises(ValueError): + registry(value) + + +if __name__ == '__main__': + unittest.main()