# -*- coding: utf-8 -*-
"""FTP Command
=================
.. module:: pcapkit.vendor.ftp.command
This module contains the vendor crawler for **FTP Command**,
which is automatically generating :class:`pcapkit.const.ftp.command.Command`.
"""
import collections
import csv
import re
import sys
from typing import TYPE_CHECKING, cast
from pcapkit.vendor.default import Vendor
if TYPE_CHECKING:
from collections import OrderedDict
from typing import Callable
__all__ = ['Command']
#: Command type.
KIND = {
'a': 'CommandType.A',
'p': 'CommandType.P',
's': 'CommandType.S',
} # type: dict[str, str]
#: Conformance requirements.
CONF = {
'm': 'ConformanceRequirement.M',
'o': 'ConformanceRequirement.O',
'h': 'ConformanceRequirement.H',
} # type: dict[str, str]
#: Default constant template of enumeration registry from IANA CSV.
LINE = lambda NAME, DOCS, ENUM, FEAT, MODL: f'''\
# -*- coding: utf-8 -*-
# mypy: disable-error-code=assignment
# pylint: disable=line-too-long
"""{(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 IntEnum, IntFlag, StrEnum, auto
from pcapkit.corekit.enum import EnumLookup, EnumRegistry
from pcapkit.corekit.sentinels import NO_DEFAULT
from pcapkit.utilities.exceptions import EnumValueError
if TYPE_CHECKING:
from typing import Any, Optional, Type
__all__ = ['{NAME}']
class FEATCode(EnumRegistry, StrEnum):
"""Keyword returned in FEAT response line for this command/extension,
c.f., :rfc:`5797#section-3`.
.. note::
Declares every FEAT keyword the IANA registry's own ``FEAT code``
column names -- the group markers below plus the per-command
keywords generated after them (data-driven, not hand-picked; see
:meth:`~pcapkit.vendor.ftp.command.Command.process`) -- rather than
minting the per-command ones at import time as an incidental side
effect of building :class:`Command`'s own rows. GitHub issue :issue:`860`:
that import-time mutation was the same defect shape :issue:`775` removed
from :class:`~pcapkit.const.pcapng.filter_type.FilterType`. ``_missing_``
still unmints for a keyword that
turns up on the wire but names none of these -- the extendability the
owner conditionally asked to keep on GitHub issue :issue:`860`: unsure what a
FEAT value absent from the RFC's list means in the CSV table, the owner
said that if the expected handling is our current approach, as industry
convention has it, the registry stays extendable as is, and then approved
declaring the registry's keywords as real members with ``_missing_``
still covering the rest. No custom ``__new__`` here, so the base's
generic :meth:`~pcapkit.corekit.enum.EnumRegistry._unregistered_member`
needs no override.
"""
#: FTP standard commands [:rfc:`0959`].
base = '<base>'
#: Historic experimental commands [:rfc:`0775`][:rfc:`1639`].
hist = '<hist>'
#: FTP Security Extensions [:rfc:`2228`].
secu = '<secu>'
#: FTP Feature Negotiation [:rfc:`2389`].
feat = '<feat>'
#: FTP Extensions for NAT/IPv6 [:rfc:`2428`].
nat6 = '<nat6>'
{FEAT}
def __repr__(self) -> 'str':
return f'<{{self.__class__.__name__}} [{{self._name_}}]>'
@classmethod
def get(cls, key: 'Any', default: 'Any' = NO_DEFAULT) -> 'FEATCode':
"""Resolve ``key`` case-insensitively, per :rfc:`5797#section-2`.
One of the few case-insensitive overrides the ruling on GitHub issue
:issue:`877` allows, and the registry's own defining document states the
comparison rule outright rather than leaving it to be inferred --
:rfc:`5797#section-2`, on the ``FEAT Code`` column this class is
generated from: *"IANA maintains uniqueness of feature names (FEAT
codes) based on case-insensitive comparison."* Two FEAT codes
therefore cannot differ only by case, so folding the caller's key
cannot resolve ambiguously.
:rfc:`2389#section-3.2`, which defines the ``FEAT`` response the
codes appear in, points the same way from the wire side: *"The
feature-label and feature-parms are nominally case sensitive,
however ... it is to be expected that those definitions will usually
specify the label and parameters in a case independent manner. Where
this is done, implementations are recommended to use upper case
letters when transmitting the feature response."* A caller feeding a
``FEAT`` response line is therefore holding upper case by
recommendation, while some of this registry's codes are registered in
lower case -- measured on the IANA CSV: ``base``, ``feat``, ``hist``,
``nat6`` and ``secu``, against all-upper-case keywords for the rest,
with none mixed. Without this override ``get('BASE')`` raised
:exc:`KeyError`, which is the defect GitHub issue :issue:`903`'s audit found.
The obvious objection, answered: :rfc:`5797` uses case *presentationally*
to tell a real keyword from a placeholder -- *"defined FEAT keywords
codes are listed in all uppercase, whereas placeholder keywords ... are
listed in lowercase"* -- so folding might look like it discards that
distinction. It does not. Only the inbound ``key`` is folded; every
member keeps the registrar's own casing, and the fold itself is backed by
the RFC: the final ruling on GitHub issue :issue:`877` is that an enum treats its
values as case-insensitive where the RFC states they are, and as
case-sensitive otherwise. So ``get('BASE').name`` is still ``'base'`` and
still says placeholder. And the uniqueness rule quoted above is what makes
that safe: a real keyword ``BASE`` could not be registered alongside the
placeholder ``base``, so there is no second member for the fold to hide.
Folds only as a *fallback*. An exact name or value hit is delegated to
:meth:`~pcapkit.corekit.enum.EnumLookup.get` untouched, so the base's
own precedence -- name before value -- and its non-minting ``str``
path both survive: nothing here calls ``cls(key)``, so a key matching
no member, folded or not, still raises rather than growing the
registry. ``get('ZZ-NOT-REAL')`` therefore still raises
:exc:`KeyError` while ``FEATCode('ZZ-NOT-REAL')`` still yields an
unregistered member, exactly as before.
Args:
key: Name or value to look up. A non-``str`` key is passed
straight through, since case cannot apply to it.
default: As :meth:`~pcapkit.corekit.enum.EnumLookup.get`. Not
folded -- it names an already-registered value rather than
arriving from the wire, so the caller spells it from this
module.
Returns:
The canonical member for ``key``, or for ``default``.
Raises:
KeyError: If no member matches ``key`` exactly or case-insensitively
and there is no usable ``default``.
ValueError: As :meth:`~pcapkit.corekit.enum.EnumLookup.get`, for a
non-``str`` key.
"""
if isinstance(key, str) and not (
key in cls._member_map_ or # pylint: disable=no-member
key in cls._value2member_map_
):
folded = key.casefold()
for name, member in cls._member_map_.items(): # pylint: disable=no-member
if name.casefold() == folded:
return member
for member in cls._value2member_map_.values():
if member.value.casefold() == folded:
return member
return super().get(key, default)
@classmethod
def _validate_value(cls, value: 'Any') -> 'None':
"""Reject ``value`` unless it is a :class:`str`, as :meth:`_missing_` does.
Overrides :meth:`~pcapkit.corekit.enum.EnumLookup._validate_value`
with the check :meth:`_missing_` makes, and the message it raises, so
:meth:`~pcapkit.corekit.enum.EnumRegistry.register` refuses exactly the
values :meth:`get` refuses, in the same words (:issue:`1816`). Any
:class:`str` passes, since :meth:`_missing_` resolves every one.
Args:
value: Value about to be looked up or minted.
Raises:
EnumValueError: If ``value`` is not a :class:`str`. A
:exc:`ValueError`, so an ``except ValueError`` caller is
unaffected.
"""
if not isinstance(value, str):
raise EnumValueError(f'{{value!r}} is not a valid {{cls.__name__}}')
@classmethod
def _missing_(cls, value: 'str') -> 'FEATCode':
"""Lookup function used when value is not found.
Args:
value: Value to get enum item.
"""
if not isinstance(value, str):
raise ValueError(f'{{value!r}} is not a valid {{cls.__name__}}')
return cls._unregistered_member(value, value.upper())
class CommandType(EnumLookup, IntFlag):
"""Type of "kind" of command, based on :rfc:`959#section-4`.
Re-parented onto :class:`~pcapkit.corekit.enum.EnumLookup` per GitHub
issue :issue:`930`, finishing :issue:`877`'s phase 2. Pure re-parenting as far as
``get``/``get_all`` are concerned -- this class defines no ``get`` of its
own to reconcile with the base -- and its own :meth:`_missing_` range
guard below is untouched, since :class:`EnumLookup` does not touch that
hook.
"""
undefined = 0
#: Access control.
A = auto()
#: Parameter setting.
P = auto()
#: Service execution.
S = auto()
@classmethod
def _missing_(cls, value: 'int') -> 'CommandType':
"""Lookup function used when value is not found.
Args:
value: Value to get enum item.
"""
if not (isinstance(value, int) and 0 <= value <= 0x07):
raise ValueError(f'{{value!r}} is not a valid {{cls.__name__}}')
return super()._missing_(value)
class ConformanceRequirement(EnumLookup, IntEnum):
"""Expectation for support in modern FTP implementations.
Re-parented onto :class:`~pcapkit.corekit.enum.EnumLookup` per GitHub
issue :issue:`930`, finishing :issue:`877`'s phase 2 -- pure re-parenting, since this
class defines neither ``get`` nor ``_missing_`` of its own to reconcile
with the base.
"""
#: Mandatory to implement.
M = auto()
#: Optional.
O = auto()
#: Historic.
H = auto()
class {NAME}(EnumRegistry, StrEnum):
"""[{NAME}] {DOCS}
.. versionchanged:: 1.5.0
Neither ``_missing_`` nor ``get()`` mints a member. The owner ruled on
GitHub issue :issue:`860` that ``get`` should not mint: only IANA-registered
values are legitimate members, a new one is properly created through
:meth:`register`, and ``get`` is not given enough information to create
one. Concretely true here -- a bare wire command word carries no
:attr:`feat`/:attr:`desc`/:attr:`type`/:attr:`conf`, so a minted member
had each of them hollowed out to its default; :meth:`register` is the
path that can actually supply them.
"""
if TYPE_CHECKING:
#: Feature code. Keyword returned in FEAT response line for this command/extension,
#: c.f., :rfc:`5797#section-2.2`.
feat: 'Optional[FEATCode]'
#: Brief description of command / extension.
desc: 'Optional[str]'
#: Type of "kind" of command, based on :rfc:`959#section-4`.
type: 'CommandType'
#: Expectation for support in modern FTP implementations.
conf: 'ConformanceRequirement'
def __new__(cls, name: 'str', feat: 'Optional[FEATCode]' = None,
desc: 'Optional[str]' = None, type: 'CommandType' = CommandType.undefined,
conf: 'ConformanceRequirement' = ConformanceRequirement.O) -> 'Type[{NAME}]':
obj = str.__new__(cls, name)
obj._value_ = name
obj.feat = feat
obj.desc = desc
obj.type = type
obj.conf = conf
return obj
def __repr__(self) -> 'str':
return f'<{{self.__class__.__name__}}.{{self._name_}}: {{self.desc}}>'
{ENUM}
@classmethod
def _unregistered_member(cls, value: 'str', name: 'str') -> '{NAME}':
"""Build a member absent from this registry's own lookup tables.
Leaves :attr:`feat`, :attr:`desc`, :attr:`type` and :attr:`conf` at
the same defaults :meth:`__new__` itself would, rather than missing
entirely --
:meth:`~pcapkit.corekit.enum.EnumRegistry._unregistered_member`
bypasses :meth:`__new__` (it calls
:class:`str`'s directly), so those attributes would otherwise
be absent and :meth:`__repr__` (which reads :attr:`desc`) would
raise on the result. There is no more specific value to reconstruct
them from -- a bare wire command word carries none of them,
which is exactly the owner's reasoning for why ``get()``/
``_missing_`` must not mint one: :meth:`register` is the path that
can actually supply them.
Args:
value: Value to get enum item -- the convention here, shared with
:class:`~pcapkit.const.ftp.command.FEATCode` and
:class:`~pcapkit.const.http.method.Method`, is the caller's
own casing, unchanged: an unregistered member's *value* is
exactly what was observed on the wire, matching how a
*registered* member's own value is exactly what was
declared, never reformatted. Only :attr:`name` -- the
identifier, not the value -- is canonicalised.
name: Bare label for the unregistered member -- here, the
canonical upper-case form of ``value``, matching the name
every *registered* member of this class is looked up by,
since :issue:`860`'s open-vocabulary registries have no manufactured
placeholder label to fall back to.
"""
obj = super()._unregistered_member(value, name)
obj.feat = None
obj.desc = None
obj.type = CommandType.undefined
obj.conf = ConformanceRequirement.O
return obj
@classmethod
def register(cls, value: 'str', name: 'str') -> '{NAME}':
"""Register a new command ``value`` under ``name``, upper-cased.
:meth:`get` and :meth:`_missing_` look a command up by its
upper-case name, so ``name`` is canonicalised to upper case here
before :meth:`~pcapkit.corekit.enum.EnumRegistry.register` mints it.
``value`` is kept as given, as a declared member's value is.
Args:
value: Value of the new member.
name: Name of the new member, in any case.
Returns:
The newly registered member.
Raises:
ValueError: As :meth:`~pcapkit.corekit.enum.EnumRegistry.register`.
"""
return super().register(value, name.upper())
@classmethod
def register_alias(cls, value: 'str', name: 'str') -> '{NAME}':
"""Add ``name``, upper-cased, as a further name for the member at ``value``.
Canonicalised for the same reason as :meth:`register`, so that
:meth:`get` finds the alias.
Args:
value: Value of the existing member to alias.
name: Alias to add for it, in any case.
Returns:
The existing member, now reachable under the upper-cased ``name``
as well.
Raises:
ValueError: As
:meth:`~pcapkit.corekit.enum.EnumRegistry.register_alias`.
"""
return super().register_alias(value, name.upper())
@classmethod
def get(cls, key: 'str', default: 'Optional[str]' = None) -> '{NAME}':
"""Backport support for original codes.
A non-:class:`str` ``key`` is delegated to
:meth:`~pcapkit.corekit.enum.EnumLookup.get`, as
:meth:`Method.get <pcapkit.const.http.method.Method.get>` delegates
every key, so :meth:`_validate_value` refuses it there rather than
``key.upper()`` being called on it (GitHub issue :issue:`1824`). It is
a :class:`classmethod`, as the base is, because a zero-argument
``super()`` needs a first argument to bind. ``{NAME}.get('X')`` binds
the same as it would on a :class:`staticmethod`.
``default`` is not forwarded on that path, as ``Method.get`` does not
forward its own: here it supplies the *value* of the unregistered
member built on a name miss, while the base's ``default`` must name a
*registered* value. A refused ``key`` therefore raises whatever
``default`` is.
.. versionchanged:: 1.5.0
A non-:class:`str` ``key`` raises
:exc:`~pcapkit.utilities.exceptions.EnumValueError`, with or
without ``default`` (:issue:`1824`). ``get(42)`` and ``get(None)``
raised :exc:`AttributeError` from ``key.upper()``, and
``get(b'ABOR')`` returned an unregistered member whose name and
value were both the :class:`bytes` object.
Args:
key: Key to get enum item. Looked up case-insensitively, as
:rfc:`959#section-5` treats FTP command codes, against member
names canonicalised to upper case on registration.
default: Value for the unregistered member built when ``key``
matches no member name. ``None``, the default, uses ``key``
itself.
Raises:
EnumValueError: If ``key`` is not a :class:`str`. This reaches the
caller from the base's non-``str`` branch, where
:meth:`_validate_value` refuses it before ``cls(key)`` could
reach :meth:`_missing_`. A :exc:`ValueError`, so an
``except ValueError`` caller is unaffected.
:meta private:
"""
if not isinstance(key, str):
# NOTE: unreachable only by the ``str`` annotation, which a caller
# at run time need not honour -- this is the path GitHub issue
# #1824 fixed.
return super().get(key) # type: ignore[unreachable]
name = key.upper()
if name not in cls._member_map_: # pylint: disable=no-member
# NOTE: the value is ``default`` if the caller supplied one, or
# else ``key`` exactly as given -- never ``name`` -- so an
# unregistered member's value is the caller's own casing, the
# same convention :meth:`_unregistered_member` documents and
# :class:`~pcapkit.const.ftp.command.FEATCode` already followed
# unchanged. Two calls naming the same command in different
# case, e.g. ``get('xyzw')`` and ``get('XYZW')``, therefore build
# results that are *not* equal -- each is exactly what its own
# caller passed. Minting instead returned, through the
# ``_member_map_`` cache, the *first* casing seen for every later
# call regardless of case; that is the one observable behaviour
# change in GitHub issue #860's conversion.
return cls._unregistered_member(default if default is not None else key, name)
return cls._member_map_[name] # pylint: disable=no-member
@classmethod
def _validate_value(cls, value: 'Any') -> 'None':
"""Reject ``value`` unless it is a :class:`str`, as :meth:`_missing_` does.
Overrides :meth:`~pcapkit.corekit.enum.EnumLookup._validate_value`
with the check :meth:`_missing_` makes, and the message it raises, so
:meth:`register` refuses a value the constructor refuses, in the same
words (:issue:`1816`). :meth:`get` reaches it as well, through the
base, for every non-:class:`str` key (:issue:`1824`), so it refuses
the same values. Any :class:`str` passes, since :meth:`_missing_`
resolves every one.
Args:
value: Value about to be looked up or minted.
Raises:
EnumValueError: If ``value`` is not a :class:`str`. A
:exc:`ValueError`, so an ``except ValueError`` caller is
unaffected.
"""
if not isinstance(value, str):
raise EnumValueError(f'{{value!r}} is not a valid {{cls.__name__}}')
@classmethod
def _missing_(cls, value: 'str') -> '{NAME}':
"""Lookup function used when value is not found.
Args:
value: Value to get enum item. Matched case-insensitively against
the canonical upper-case member names.
"""
if not isinstance(value, str):
raise ValueError(f'{{value!r}} is not a valid {{cls.__name__}}')
name = value.upper()
if name in cls._member_map_:
return cls._member_map_[name] # type: ignore[return-value]
return cls._unregistered_member(value, name)
'''.strip() # type: Callable[[str, str, str, str, str], str]
[docs]
class Command(Vendor):
"""FTP Command"""
#: Link to registry.
LINK = 'https://www.iana.org/assignments/ftp-commands-extensions/ftp-commands-extensions-2.csv'
def process(self, data: 'list[str]') -> 'tuple[list[str], list[str]]':
"""Process CSV data.
Args:
data: CSV data.
Returns:
:class:`Command`'s enumeration fields, and every distinct
per-command keyword the ``FEAT code`` column names --
:class:`FEATCode` must declare these as real members (GitHub
issue :issue:`860`), rather than the old approach of minting one as an
incidental side effect the first time a :class:`Command` row
referencing it was evaluated at import time.
"""
reader = csv.reader(data)
next(reader) # header
enum = collections.OrderedDict() # type: OrderedDict[str, str]
feat_codes = collections.OrderedDict() # type: OrderedDict[str, str]
for item in reader:
cmmd = item[0].strip('+')
feat = item[1] or None
desc = re.sub(r'{.*}', r'', item[2]).strip() or None
kind = ' | '.join(KIND[s] for s in item[3].split('/') if s in KIND) or None
conf = CONF.get(item[4].split()[0])
temp = [] # type: list[str]
#for rfc in filter(lambda s: 'RFC' in s, re.split(r'\[|\]', item[5])):
# temp.append(f'[{rfc[:3]} {rfc[3:]}]')
for rfc in map(str.strip, self.split_references(item[5])):
if 'RFC' in rfc and re.match(r'\d+', rfc[3:]):
# NOTE: IANA has put a section inside the citation since
# 2026-10-13 -- ``ALGS`` cites ``[RFC 6384, Section 11]``,
# formerly ``[RFC6384] Section 11`` -- and the whole of
# ``rfc[3:]`` would make that a role no RFC answers (#1664).
num, _, sec = rfc[3:].partition(', Section ')
temp.append(f'[:rfc:`{num}#section-{sec}`]' if sec else f'[:rfc:`{num}`]')
else:
temp.append(f'[{rfc}]'.replace('_', ' '))
cmmt = self.wrap_comment(f'{desc} {"".join(temp)}')
if cmmd == '-N/A-':
cmmd = cast('str', feat)
if isinstance(feat, str):
# NOTE: GitHub issue #860. Every FEAT code the registry
# names -- lower-case group marker or upper-case per-command
# keyword alike -- is a declared member of FEATCode (see
# feat_codes below and FEATCode's own template), so both
# cases resolve by plain attribute access, and nothing calls
# FEATCode(...) at import time, which would mint the
# upper-case ones as a side effect of evaluating this very
# module.
if feat.isupper() and feat not in feat_codes:
feat_codes[feat] = f'#: {cmmt}\n {feat} = {feat!r}'
feat = f'FEATCode.{feat}'
pres = f"{cmmd}: 'Command' = {cmmd!r}, {feat}, {desc!r}, {kind or 0}, {conf}"
sufs = f'#: {cmmt}'
enum[cmmd] = f'{sufs}\n {pres}'
return list(enum.values()), list(feat_codes.values())
def context(self, data: 'list[str]') -> 'str':
"""Generate constant context.
Args:
data: CSV data.
Returns:
Constant context.
"""
enum, feat_codes = self.process(data)
ENUM = '\n\n '.join(map(lambda s: s.rstrip(), enum)).strip()
FEAT = '\n\n '.join(map(lambda s: s.rstrip(), feat_codes)).strip()
return LINE(self.NAME, self.DOCS, ENUM, FEAT, self.__module__)
if __name__ == '__main__':
sys.exit(Command()) # type: ignore[arg-type]