Source code for pcapkit.vendor.reg.linktype

# -*- coding: utf-8 -*-
"""Link-Layer Header Type Values
===================================

.. module:: pcapkit.vendor.reg.linktype

This module contains the vendor crawler for **Link-Layer Header Type Values**,
which is automatically generating :class:`pcapkit.const.reg.linktype.LinkType`.

"""

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 bs4.element import Tag

__all__ = ['LinkType']


[docs] class LinkType(Vendor): """Link-Layer Header Type Values""" #: Value limit checker. FLAG = 'isinstance(value, int) and 0x00000000 <= value <= 0xFFFFFFFF' #: Link to registry. LINK = 'http://www.tcpdump.org/linktypes.html' def count(self, data: 'list[str]') -> 'Counter[str]': """Count field records.""" return collections.Counter() def request(self, text: 'str') -> 'list[Tag]': # type: ignore[override] # pylint: disable=signature-differs """Fetch registry table. Args: text: Context from :attr:`~LinkType.LINK`. Returns: Rows (``tr``) from registry table (``table``). """ soup = bs4.BeautifulSoup(text, 'html5lib') table = soup.select('table.linktypedlt')[0] return table.select('tr')[1:] def process(self, data: 'list[Tag]') -> 'tuple[list[str], list[str]]': """Process registry data. Args: data: Registry data. Returns: Enumeration fields and missing fields. """ enum = [] # type: list[str] legacy = [] # type: list[str] miss = [ "return cls._unregistered_member(value, 'Unassigned')", ] # Value-aware guard for the ``sink`` decision below (GitHub issue #852, # a follow-up to #844). A row's notes mentioning "legacy" is # tcpdump's only textual signal for *which* member of a duplicated # value is the deprecated one, but that word can appear in a row's # notes for an unrelated reason -- e.g. referencing some other, # differently-valued code as "the legacy DLT_FOO". Computing which # values are genuinely duplicated in this table upfront, independent # of row order, stops the sink from firing on a value that has no # duplicate to disambiguate in the first place. It does not, on its # own, guarantee which member of a *genuine* duplicate pair is # correct if tcpdump's own wording is attached to the wrong one of # the two -- that residual case still relies on the wording being # trustworthy. # # A range row (``DLT_USER0``..``DLT_USER15``) is never itself routed # through ``sink`` (see item 2 in #852), but its *expanded* values # still have to count here: a single-value row can legitimately # duplicate one of the values a range expands to, and missing that # would silently under-count the duplicate the same way the # word-only rule did before #852. # # The single-value branch accepts whatever ``int()`` accepts -- # a leading sign, PEP 515 underscores -- rather than the narrower # ``str.isdigit()``, so it recognises exactly the same values the # main loop's own ``int(temp)`` below does. A signed or underscored # value is not present in tcpdump's live table (every # committed member matches a plain unsigned decimal), but a # narrower recognition set here than in the main loop would be the # same class of silent under-count this pre-scan exists to close # for ranges: a legacy row's value would go uncounted, so its alias # would stop being sunk without anything failing loudly. def _expand(temp: 'str') -> 'list[int]': try: return [int(temp)] except ValueError: pass if '–' in temp: # en dash, not a hyphen -- see the ``except ValueError`` below try: start, stop = map(int, temp.split('–')) except ValueError: return [] return list(range(start, stop + 1)) return [] value_counts = collections.Counter( value for temp in (content.select('td.number')[0].text.strip() for content in data) for value in _expand(temp) ) # type: Counter[int] dup_values = {value for value, count in value_counts.items() if count > 1} for content in data: name = content.select('td.symbol')[0].text.strip()[9:].strip() temp = content.select('td.number')[0].text.strip() desc = content.select('td.symbol')[1].text.strip() cmmt = re.sub(r'\s+', ' ', content.select('td')[3].text.strip()).replace("''", '``').replace('_', r'\_') if not name: name = desc[4:] try: code, code_int = temp, int(temp) if not name: name = f'Unassigned_{code}' pres = f"{name} = {code}" if desc: sufs = "#: %s" % self.wrap_comment(f"[``{desc}``] {cmmt}") # pylint: disable=consider-using-f-string else: sufs = "#: %s" % self.wrap_comment(cmmt) # if len(pres) > 74: # sufs = f"\n{' '*80}{sufs}" # enum.append(f'{pres.ljust(76)}{sufs}') # Sink this row after every current entry only when its value # is a genuine duplicate (``dup_values`` above) *and* its own # notes mention "legacy", so the current name -- not the # legacy one -- is the first member defined for that value # and therefore wins ``LinkType(value).name``. sink = legacy if (code_int in dup_values and 'legacy' in cmmt.lower()) else enum sink.append(f'{sufs}\n {pres}') except ValueError: start, stop = map(int, temp.split('–')) for code in range(start, stop+1): name = f'USER{code-start}' desc = f'DLT_USER{code-start}' pres = f"{name} = {code}" sufs = "#: %s" % self.wrap_comment(f"[``{desc}``] {cmmt}") # pylint: disable=consider-using-f-string # if len(pres) > 74: # sufs = f"\n{' '*80}{sufs}" # enum.append(f'{pres.ljust(76)}{sufs}') # Range rows are generic, user-definable placeholders, # never a deprecated/current pair -- so every expanded # member always lands in ``enum`` unconditionally, # rather than through the per-row ``sink`` above. # Otherwise one range row whose notes happened to mention # "legacy" would sink every member it expands to at # once instead of just itself (item 2 in #852). enum.append(f'{sufs}\n {pres}') if legacy: # A reader scanning the generated file by value will find these # members out of numeric order: sinking appends them here, after # every current entry, rather than leaving them at their # original position in tcpdump's table (item 4 in #852). Say so # once, right where they are emitted -- as a plain ``#`` comment, # deliberately, not a ``#:`` one: a ``#:`` line immediately above # an attribute becomes that attribute's rendered Sphinx docstring, # and this note is about the generator's own source layout, not # about what ``IPMB_LINUX`` (or whichever name is sunk here) means # to a caller. legacy[0] = ( "# The following legacy alias(es) are emitted here, after every\n" " # current entry above, instead of at their original numeric\n" " # position -- see pcapkit.vendor.reg.linktype.LinkType.process\n" " # for why.\n" " " + legacy[0] ) return enum + legacy, miss
if __name__ == '__main__': sys.exit(LinkType()) # type: ignore[arg-type]