Source code for pcapkit.vendor.ipx.socket

# -*- coding: utf-8 -*-
# pylint: disable=wrong-import-position
"""Socket Types
==================

.. module:: pcapkit.vendor.ipx.socket

This module contains the vendor crawler for **Socket Types**,
which is automatically generating :class:`pcapkit.const.ipx.socket.Socket`.

"""

###############################################################################
# NOTE: fix duplicated name of ``socket```
import sys

path = sys.path.pop(0)
###############################################################################

import collections
from typing import TYPE_CHECKING

from pcapkit.vendor.default import Vendor

if TYPE_CHECKING:
    from collections import Counter

###############################################################################
sys.path.insert(0, path)
###############################################################################

__all__ = ['Socket']

###############################################################################
# NOTE: this crawler does not crawl: the registry below is maintained by hand;
# see #507.
#
# Its source was the well-known-socket table in
# https://en.wikipedia.org/wiki/Internetwork_Packet_Exchange#Socket_number,
# and two separate things broke that. Wikipedia answers ``requests``'
# default ``python-requests/<version>`` User-Agent with HTTP 403 (measured
# 2026-09-19: 403 and 126 bytes of robot-policy text for the default agent,
# 200 and 114117 bytes for a descriptive one), so ``Vendor._request`` could not
# fetch the page at all. And the table itself was deleted from the article on
# 2026-08-25, in revision 1371327031 ("deleted some irrelevant technical
# tables"), leaving the page with a single ``wikitable`` -- the IPX header
# format one -- so that ``find_all('table', class_='wikitable')[3]`` raises
# ``IndexError`` even once the fetch is fixed.
#
# Pointing ``LINK`` at the last revision that still carries the table
# (``oldid=1368657333``) would have worked, but it buys none of what a crawler
# is for: a network fetch pinned to a frozen snapshot is all of the fragility
# with none of the freshness, and it would still be selecting the table by
# index out of a document that nothing stops from changing shape again. The
# registry itself is closed, which is what settles it -- IPX socket numbers
# were Novell's to assign, and Novell's own documentation still ends the list
# with "software developers writing NetWare applications can contact Novell to
# reserve well-known sockets". No Novell remains to contact, and IPX
# will not gain new socket numbers, so there is nothing for a crawler to pick
# up on its next run.
#
# The table below is therefore transcribed from that last revision that carried
# it, row for row, and each entry names the primary source for the assignment
# where one exists.
###############################################################################

#: Socket number registry, transcribed from the last revision of the Wikipedia
#: article that still carried the table (2026-08-10),
#: https://en.wikipedia.org/w/index.php?title=Internetwork_Packet_Exchange&oldid=1368657333
#:
#: Maps a socket number to the enumeration name it takes and the comment
#: rendered above it. Names go through :meth:`~Vendor.rename`, so the generated
#: member is the :meth:`~Vendor.safe_name` of the first element.
DATA = {
    # NOTE: 0x0000 was never listed as a well-known socket in the scraped
    # registry table, but it is the IPX protocol's own default for the
    # ``dst``/``src`` socket field (an ordinary "unspecified socket"), so it
    # must be present regardless of what the registry says; see #492.
    #
    # That is a statement about the *scraped table*, not about the number being
    # unsourced. XSIS 028112 Appendix D (cited under RANGES below) reserves it
    # in the primary standard: "The socket numbers zero and all ones are
    # reserved to mean unknown and all, respectively." The same sentence
    # reserves all-ones -- 0xFFFF, to mean "all" -- and this table has no entry
    # for that; ``_missing_`` resolves 0xFFFF through the "Dynamically Assigned"
    # range instead. Adding it would change the generated file, so it is left as
    # a follow-up rather than done here.
    0x0000: ('Unspecified', "Unspecified socket; this is IPX's own default for the dst/src socket field."),

    # Sockets in the "Registered by Xerox" range below. These are formally
    # XNS/IDP well-known sockets that IPX inherited, and the *values* have both
    # a live registry and a primary standard behind them:
    #
    # * IANA, "Xerox Network System (XNS) Protocol Types", sub-registry
    #   "Assigned well-known socket numbers" --
    #   https://www.iana.org/assignments/xns-protocol-types -- lists 1 Routing
    #   Information, 2 Echo and 3 Router Error, each referenced to "Xerox System
    #   Integration Standard: Internet Transport Protocols. XSIS 028112,
    #   December 1981". IANA publishes the registry but does not own it: its
    #   registration procedure reads "Not assigned by IANA".
    # * That standard itself, Appendix D, "Assigned well-known socket numbers",
    #   scanned at http://www.bitsavers.org/pdf/xerox/xns/standards/XSIS_028112-Internet_Transport_Protocols_198112.pdf
    #   -- the same three functions, under a column headed "Well-Known Socket
    #   (octal)".
    #
    # The *names* are a different matter, and are the archived revision's own.
    # Both sources call socket 3 "Router Error" rather than "Error Handling
    # Packet", and neither appends "Packet" to any of the three. That suffix
    # looks like the adjacent packet-type registry bleeding in -- XSIS Appendix
    # E, "Assigned internet packet types", and the second table on the same IANA
    # page, which do use 1 Routing Information / 2 Echo / 3 Error as *packet*
    # types. The names are kept as transcribed anyway, for the same reason
    # ``LLC_4`` is kept below: changing one renames a public member.
    0x0001: ('Routing Information Packet', 'Routing Information Packet'),
    0x0002: ('Echo Protocol Packet', 'Echo Protocol Packet'),
    0x0003: ('Error Handling Packet', 'Error Handling Packet'),

    # Novell, *IPX Addressing*, "Table 2. NetWare Socket Numbers and
    # Processes", https://www.novell.com/documentation/nw6p/ipx_enu/data/hvvqznoa.html
    # -- still live. That page is also the citation the article itself used.
    #
    # NOTE: Table 2 has **seven** rows and not all of them are below. In full
    # it reads 0x451 NCP, 0x452 SAP, 0x453 RIP, 0x455 Novell NetBIOS, 0x456
    # Diagnostics, 0x9001 NLSP, 0x9004 "IPXWAN(TM) protocol". The last two have
    # never been in this enumeration, and 0x9004 is independently reserved by
    # :rfc:`1634` line 552 -- "The socket number 0x9004 is a Novell reserved
    # socket number for exclusive use with IPX WAN protocol exchange" -- and by
    # the two RFCs it obsoletes: :rfc:`1551` line 532 word for word, and
    # :rfc:`1362` line 253 with "information exchange" for "protocol exchange".
    # So they are known omissions with good sourcing, not rows nobody noticed.
    # Adding them would add members to the generated file and so give up the
    # byte-identity proof this change rests on, which makes them a follow-up
    # rather than part of #507.
    #
    # Some of the rows below also depart from Novell's own wording: Novell writes
    # "Novell NetBIOS" and "Diagnostics" where this table says "NetBIOS" and
    # "Diagnostic Packet". Those are the archived revision's strings, kept for
    # the same member-renaming reason as everything else here.
    0x0451: ('NetWare Core Protocol', 'NetWare Core Protocol, NCP – used by Novell NetWare servers'),
    0x0452: ('Service Advertising Protocol', 'Service Advertising Protocol, SAP'),
    0x0453: ('Routing Information Protocol', 'Routing Information Protocol, RIP'),
    0x0455: ('NetBIOS', 'NetBIOS'),
    0x0456: ('Diagnostic Packet', 'Diagnostic Packet'),

    # These are the only rows the archived revision alone supplies: absent
    # from Novell's Table 2, absent from the Xerox registry above, and absent
    # from every RFC swept. The sweep covered RFCs 1132, 1362, 1377, 1551, 1552,
    # 1553, 1634, 1791, 1973 and 2043, looking for the codes 0x0001, 0x0002,
    # 0x0003, 0x0457 and 0x4003, and found 0 hits. (0x0001, 0x0002 and 0x0003
    # turned out to be sourced after all, just not from an RFC -- see the Xerox
    # note above.)
    #
    # "0 hits" is a claim about those codes and nothing more. Some of the
    # RFCs swept do assign an IPX socket, 0x9004, which this table lacks; see the
    # note on Novell's Table 2 above.
    #
    # 0x4003 is the weaker of the two: it falls inside Novell's own
    # 0x4000-0x7FFF *dynamic* range, which is assigned to workstations on
    # demand, so a fixed well-known name for it is inherently low-confidence.
    0x0457: ('Serialization Packet', 'Serialization Packet, used for NCP as well'),
    0x4003: ('Used by Novell NetWare Client', 'Used by Novell NetWare Client'),

    # NOTE: :rfc:`1132`, "A Standard for the Transmission of 802.2 Packets over
    # IPX Networks", reserves this one -- "The IPX socket 0x8060 has been
    # reserved by Novell for the implementation of this protocol."
    #
    # The archived table renders the row as ``LLC[4]``, where ``[4]`` is
    # Wikipedia's own footnote marker for its citation of that RFC rather than
    # any part of the protocol name -- the cell markup is an ``<a>`` reading
    # "LLC" followed by ``<sup id="cite_ref-RFC1132_4-1">``. The spaces in
    # ``LLC [ 4 ]`` are not in the page at all: they are an artefact of the
    # scrape, which joined the cell's strings with a space and so picked up the
    # ``<span class="cite-bracket">`` wrappers around the ``[`` and ``]`` as
    # separate tokens. The value is right and the explanation of it was not.
    # Both the name and the comment are kept verbatim so that
    # regenerating the constant file stays a no-op against the last scraped
    # output: renaming the member would break
    # :attr:`pcapkit.const.ipx.socket.Socket.LLC_4` for anyone using it, which
    # is a call for the maintainer rather than for #507.
    0x8060: ('LLC_4', 'LLC [ 4 ]'),

    # :rfc:`1791`, "TCP And UDP Over IPX Networks With Fixed Path MTU" -- "The
    # IPX socket number 0x9091 is reserved for the TCP", "UDP must send and
    # receive the packets on IPX/IPXF socket 0x9092", and "IPXF fragments are
    # received by IPXF on the IPX socket 0x9093".
    #
    # NOTE: the name ``TCP over IPXF`` is wrong, and it is the archived
    # revision's wording rather than the RFC's. RFC 1791 reserves 0x9091 for TCP
    # over plain **IPX**: the section that assigns it is s3, "Running TCP Over
    # IPX", which opens "Unlike UDP, TCP runs directly over IPX", and the same
    # section ends "Hence, running TCP over IPXF is not recommended." So the
    # member name describes the one arrangement the cited RFC advises against.
    # Only the name is wrong; 0x9091 is the right value for TCP. It is kept for
    # the member-renaming reason set out above, and is the strongest candidate in
    # this table for a rename with a deprecated alias.
    #
    # ``UDP over IPXF`` for 0x9092, by contrast, is accurate: "UDP must run on
    # IPXF rather than directly on IPX", and the socket is named as
    # "IPX/IPXF socket 0x9092".
    #
    # One more scruple about wording: "reserved" is the RFC's own word for
    # 0x9091 only. For 0x9092 and 0x9093 it says packets "must" be sent and
    # received on them, which amounts to the same reservation in practice but is
    # an inference rather than a quote.
    0x9091: ('TCP over IPXF', 'TCP over IPXF'),
    0x9092: ('UDP over IPXF', 'UDP over IPXF'),
    0x9093: ('IPXF', 'IPXF, IPX Fragmentation Protocol'),
}  # type: dict[int, tuple[str, str]]

#: Socket number ranges, transcribed from the same revision as
#: :data:`~pcapkit.vendor.ipx.socket.DATA`, as ``(start, stop, name)``.
#:
#: Some of these ranges have a primary source, and it is not the archived
#: revision. Xerox, *Internet Transport Protocols*, XSIS 028112, December 1981,
#: Appendix D: "The socket numbers zero and all ones are reserved to mean
#: unknown and all, respectively. Well-known socket numbers have the range 1 to
#: 3000 decimal. All other socket numbers are ephemeral, that is, they may be
#: dynamically assigned and reused." Its table then gives "Experimental 40-77"
#: under a column header that the scan OCRs as ``goctalf`` and which reads
#: "Well-Known Socket (octal)".
#:
#: That reproduces the Xerox ranges exactly, and the arithmetic confirms
#: the radix without having to trust that OCR: 3000 decimal is 0x0BB8, 3001 is
#: 0x0BB9, and octal 40-77 is 0x0020-0x003F -- each as transcribed. Read as
#: decimal instead, 40-77 would be 0x0028-0x004D, which is not what the table
#: says. So the octal reading is settled by the numbers themselves.
#:
#: Novell's *IPX Addressing* bears on the remaining ranges, in opposite directions.
#: ``(0x8000, 0xFFFF)`` matches it exactly -- "Socket numbers between 0x8000 and
#: 0xFFFF are well-known sockets; these are assigned by Novell to specific
#: processes." ``(0x4000, 0x4FFF)`` is *contradicted*, not merely rounded: the
#: preceding sentence reads "Socket numbers between 0x4000 and 0x7FFF are
#: dynamic sockets", so the upper bound is 0x7FFF. The transcribed 0x4FFF is
#: kept because widening it would change the generated ``_missing_``.
RANGES = [
    # NOTE: order is significant. The generated ``_missing_`` tests these in
    # sequence and returns on the first match, so a wide range placed before a
    # narrower one it fully contains would mask that narrower one -- it would
    # never be reached, and the socket would be misdescribed under the wide
    # range's name instead. See GitHub issue #841, which found exactly that:
    # ``(0x0020, 0x003F)`` is a strict subset of ``(0x0001, 0x0BB8)``, and both
    # ``(0x4000, 0x4FFF)`` and ``(0x8000, 0xFFFF)`` are strict subsets of
    # ``(0x0BB9, 0xFFFF)``. Each subset range is therefore listed ahead of the
    # wider range that contains it, rather than in the order the rows appeared
    # in the archived revision.
    (0x0020, 0x003F, 'Experimental'),
    (0x0001, 0x0BB8, 'Registered by Xerox'),
    (0x4000, 0x4FFF, 'Dynamically Assigned Socket Numbers'),
    (0x8000, 0xFFFF, 'Statically Assigned Socket Numbers'),
    (0x0BB9, 0xFFFF, 'Dynamically Assigned'),
]  # type: list[tuple[int, int, str]]

#: Range names from :data:`RANGES` above that name an allocation *policy* for
#: the pool (who may claim a code, or that nobody has) rather than a specific
#: assigned protocol. The maintainer's test, set in review of the branch-order
#: fix for :issue:`841`, is whether a label is the final concrete assigned name or only
#: a notation for readers; the ruling on :issue:`775` kept ``Registered by Xerox`` out
#: of this set on that test, because a proprietary protocol may expose no name
#: of its own, so the company name serves as its name. :issue:`775`'s final round
#: converts it too, so this set does not decide *whether* a range registers --
#: only *how its name is spelled*: these four keep the bare label with no hex
#: suffix, since the label itself already says nothing was assigned, while
#: ``Registered by Xerox`` keeps the hex-suffixed name it always rendered.
UNASSIGNED_RANGE_NAMES = frozenset({
    'Experimental',
    'Dynamically Assigned Socket Numbers',
    'Statically Assigned Socket Numbers',
    'Dynamically Assigned',
})


[docs] class Socket(Vendor): """Socket Types""" #: Value limit checker. FLAG = 'isinstance(value, int) and 0x0000 <= value <= 0xFFFF' def request(self) -> 'dict[int, tuple[str, str]]': # type: ignore[override] # pylint: disable=arguments-differ """Fetch registry data. Returns: Registry data (:data:`~pcapkit.vendor.ipx.socket.DATA`). """ return DATA def count(self, data: 'dict[int, tuple[str, str]]') -> '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 data.values()) def process(self, data: 'dict[int, tuple[str, str]]') -> 'tuple[list[str], list[str]]': # type: ignore[override] """Process registry data. Args: data: Registry data. Returns: Enumeration fields and missing fields. """ enum = [] # type: list[str] miss = [] # type: list[str] for code, (name, desc) in data.items(): pval = f'0x{code:04X}' renm = self.rename(name, pval) enum.append(f'#: {self.wrap_comment(desc)}\n {renm} = {pval}') # NOTE: the range names are emitted raw, spaces and all, rather than # through ``safe_name``, because that is what the scrape did and what # the shipped constant file contains; sanitising them here would rename # every member ``_missing_`` extends the enumeration with; see #507. for start, stop, name in RANGES: miss.append(f'if 0x{start:04X} <= value <= 0x{stop:04X}:') miss.append(f' #: {self.wrap_comment(name)}') if name in UNASSIGNED_RANGE_NAMES: miss.append(f" return cls._unregistered_member(value, '{name}')") else: miss.append(f" return cls._unregistered_member(value, '{name}_0x%s' % hex(value)[2:].upper().zfill(4))") return enum, miss
if __name__ == '__main__': sys.exit(Socket()) # type: ignore[arg-type]