# -*- coding: utf-8 -*-
"""Radiotap Flags
====================
.. module:: pcapkit.vendor.radiotap.flags
This module contains the vendor crawler for **Radiotap Flags**,
which is automatically generating :class:`pcapkit.const.radiotap.flags.Flags`.
Each radiotap bitmap radiotap.org defines bit by bit has a flag registry, and
each registry's crawler scrapes the table on its field's page.
:class:`Flags` holds the scraping every one of them shares, and the crawlers of
the other bitmaps subclass it, naming their page and, where it holds more than
one bitmap, the subfield.
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. 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
``include/net/ieee80211_radiotap.h`` [#]_ defines for the bit, less its
``IEEE80211_RADIOTAP_`` or ``IEEE80211_`` prefix. Where Linux defines none, the
macro is Wireshark's, from ``packet-ieee80211-radiotap-defs.h`` [#]_ or
``packet-ieee80211-radiotap.c`` [#]_; where neither does, the member takes
radiotap.org's wording of the bit, as :meth:`~Vendor.safe_name` renders it.
.. [#] Linux at commit ``e5fefa3da5bf``,
https://github.com/torvalds/linux/blob/e5fefa3da5bf7c45bef90a3cca6a49e6c2d80877/include/net/ieee80211_radiotap.h
.. [#] Wireshark at commit ``090e75fa5daa``,
https://gitlab.com/wireshark/wireshark/-/blob/090e75fa5daa8ec6ffc35df00c3a93c281458ece/epan/dissectors/packet-ieee80211-radiotap-defs.h
.. [#] Wireshark at commit ``aae6da75c524``,
https://gitlab.com/wireshark/wireshark/-/blob/aae6da75c524dc8525373ae1a2bd634592e0e278/epan/dissectors/packet-ieee80211-radiotap.c
"""
import collections
import re
import sys
from typing import TYPE_CHECKING
import bs4
from pcapkit.vendor.default import Vendor
if TYPE_CHECKING:
from collections import Counter
from typing import Callable, List, Optional, Tuple
from bs4.element import Tag
#: Text before a table, its column names and its rows, as
#: :meth:`Flags.request` returns each table.
Table = Tuple[str, List[str], List[List[str]]]
__all__ = ['Flags']
LINE = lambda NAME, DOCS, FLAG, ENUM, MODL: f'''\
# -*- coding: utf-8 -*-
# pylint: disable=line-too-long,consider-using-f-string
"""{(name := DOCS.split(' [', maxsplit=1)[0])}
{'=' * (len(name) + 6)}
.. module:: {MODL.replace('vendor', 'const')}
This module contains the constant enumeration for **{name}**,
which is automatically generated from :class:`{MODL}.{NAME}`.
"""
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__ = ['{NAME}']
class {NAME}(EnumRegistry, IntFlag):
"""[{NAME}] {DOCS}"""
{ENUM}
@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 ({FLAG}):
raise EnumValueError('%r is not a valid %s' % (value, cls.__name__))
@classmethod
def _missing_(cls, value: 'int') -> '{NAME}':
"""Lookup function used when value is not found.
Args:
value: Value to get enum item.
"""
if not ({FLAG}):
raise ValueError('%r is not a valid %s' % (value, cls.__name__))
return super()._missing_(value)
''' # type: Callable[[str, str, str, str, str], str]
#: Meanings radiotap.org gives a bit it leaves undefined, lower-cased.
RESERVED = re.compile(r'\(?(reserved|unused)\b')
#: Meaning radiotap.org gives a bit that means what the first column says.
SAME = '(same)'
#: A mask, as radiotap.org writes one in a table's first column.
MASK = re.compile(r'0x[0-9a-fA-F]+')
#: Prefixes of the source macros, dropped from a member's name, longest first.
PREFIXES = ('IEEE80211_RADIOTAP_', 'IEEE80211_')
[docs]
class Flags(Vendor):
"""Radiotap Flags"""
#: Value limit checker.
FLAG = 'isinstance(value, int) and 0 <= value <= 0xFF'
#: Link to registry.
LINK = 'https://www.radiotap.org/fields/Flags.html'
#: 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.
NAMES = {
0x01: 'IEEE80211_RADIOTAP_F_CFP',
0x02: 'IEEE80211_RADIOTAP_F_SHORTPRE',
0x04: 'IEEE80211_RADIOTAP_F_WEP',
0x08: 'IEEE80211_RADIOTAP_F_FRAG',
0x10: 'IEEE80211_RADIOTAP_F_FCS',
0x20: 'IEEE80211_RADIOTAP_F_DATAPAD',
0x40: 'IEEE80211_RADIOTAP_F_BADFCS',
0x80: 'IEEE80211_RADIOTAP_F_SHORTGI',
} # type: dict[int, str]
def request(self, text: 'str') -> 'list[Table]': # type: ignore[override] # pylint: disable=signature-differs
"""Fetch registry tables.
Args:
text: Context from :attr:`~Flags.LINK`.
Returns:
Each table of :attr:`SUBFIELD`, as the text before it, its column
names and its rows of cell texts. A table with no header row gets
the column names ``mask`` and ``meaning``.
"""
soup = bs4.BeautifulSoup(text, 'html5lib')
tables = [] # type: list[Table]
for table in soup.select('table'):
caption, subfield = self._caption(table)
if self.SUBFIELD is not None and subfield != self.SUBFIELD:
continue
rows = [[' '.join(' '.join(cell.stripped_strings).replace('«BR»', ' ').split())
for cell in row.select('td, th')] for row in table.select('tr')]
if rows and MASK.fullmatch(rows[0][0]):
tables.append((caption, ['mask', 'meaning'], rows))
else:
tables.append((caption, rows[0], rows[1:]))
return tables
def count(self, data: 'list[Table]') -> 'Counter[str]': # type: ignore[override]
"""Count field records.
Args:
data: Registry data.
Returns:
Field recordings.
"""
return collections.Counter(self.safe_name(name) for _, name, _ in self._members(data))
def process(self, data: 'list[Table]') -> 'list[str]': # type: ignore[override]
"""Process registry data.
Args:
data: Registry data.
Returns:
Enumeration fields.
"""
enum = [] # type: list[str]
for mask, name, desc in self._members(data):
macro = self.NAMES.get(mask)
if macro is None:
renm = self.rename(name, f'0x{mask:x}')
else:
renm = next(macro[len(prefix):] for prefix in PREFIXES if macro.startswith(prefix))
enum.append(f'#: {self.wrap_comment(desc)}\n {renm} = 1 << {mask.bit_length() - 1}')
return enum
def context(self, data: 'list[Table]') -> 'str': # type: ignore[override]
"""Generate constant context.
Args:
data: Registry data.
Returns:
Constant context.
"""
ENUM = '\n\n '.join(map(lambda s: s.rstrip(), self.process(data)))
return LINE(self.NAME, self.DOCS, self.FLAG, ENUM, self.__module__)
def _caption(self, table: 'Tag') -> 'tuple[str, Optional[str]]':
"""Name the subfield a table describes.
Args:
table: The table.
Returns:
The text of the paragraph before the table, and the subfield it
names: the ``<code>`` word of a paragraph ``The <code>x</code>
field ...``, or the text of the heading above the table.
"""
caption = ''
for elem in table.find_all_previous(['p', 'h2']):
text = ' '.join(elem.get_text().split())
caption = caption or text
if elem.name == 'h2':
return caption, text
code = elem.find('code')
if code is not None and text.startswith(f'The {code.get_text()} '):
return caption, code.get_text()
return caption, None
def _members(self, data: 'list[Table]') -> 'list[tuple[int, str, str]]':
"""Pick the members out of the tables.
Args:
data: Registry data.
Returns:
Each member's mask, name and description, in mask order.
"""
members = {} # type: dict[int, tuple[str, str]]
for number, (caption, header, rows) in enumerate(data):
for row in rows:
if not MASK.fullmatch(row[0]):
continue
mask = int(row[0], 16)
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']
notes = [text for column, text in zip(header[1:], row[1:]) if column.lower() == 'notes' and text]
first = meanings[0][1]
defined = [(column, first if text == SAME else text) for column, text in meanings
if not RESERVED.match((first if text == SAME else text).lower())]
if not defined:
continue
name = defined[0][1]
if len(defined) == len(meanings) and len({text for _, text in defined}) == 1:
desc = name
else:
# NOTE: a page with a column per PPDU kind defines some bits
# for some kinds only, so the comment names the kinds.
kinds = collections.defaultdict(list) # type: dict[str, list[str]]
for column, text in defined:
kinds[text].append(column)
desc = '; '.join(f'{", ".join(columns)}: {text}' for text, columns in kinds.items())
if notes:
desc = f'{desc} -- {" ".join(notes)}'
if number:
desc = f'{desc} [{caption.rstrip(":")}]'
members[mask] = (name, desc)
return [(mask, name, desc) for mask, (name, desc) in sorted(members.items())]
if __name__ == '__main__':
sys.exit(Flags()) # type: ignore[arg-type]