From 800a6ec4b37dddf70814fdcdbcd859c086346488 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Mon, 26 Oct 2026 16:43:41 -0400 Subject: [PATCH] fix(ftp): place FTP_DATA in the application layer, in its own module (#2285) FTP_DATA derived from Raw alone, so its __layer__ was None, though ext.rst lists it under the application layer and the layer-placement convention puts the layer base first. A layer='application' extraction never stopped at it, and extract(layer=FTP_DATA) resolved to 'none'. Declare it class FTP_DATA(Application, Raw). Raw still does the parsing and rebuild; __index__ is Application's, raising IntError. FTP and FTP_DATA are reached under different AppType port entries, unlike RARP/DRARP's single shared destination, so FTP_DATA moves to its own module (ftp_data.py) rather than staying a sibling in ftp.py -- a pure move, since it needed nothing ftp.py's other imports carry. The old path keeps nothing behind it (#1744's no-re-exports rule); a versionremoved note on both the docstring and the docs page records where it shipped (v1.4.0-v1.4.1). Tests: FTP_DATA's bases, layer, Raw behaviour and layer-limited extraction in the layer-placement tests; the pcapkit.all grouping check no longer excuses an unset __layer__; the move is pinned by the #1744 no-re-export ledger and the #1739 module-identity sweep. --- .../conventions/protocol-layer-placement.rst | 4 +- docs/source/contributing/pep.rst | 2 +- docs/source/ext.rst | 2 +- .../pcapkit/protocols/application/ftp.rst | 18 +++--- .../protocols/application/ftp_data.rst | 29 +++++++++ .../pcapkit/protocols/application/index.rst | 1 + docs/source/pcapkit/protocols/index.rst | 4 +- examples/generators/dispatch.py | 2 +- pcapkit/protocols/application/__init__.py | 3 +- pcapkit/protocols/application/ftp.py | 30 ++++----- pcapkit/protocols/application/ftp_data.py | 64 +++++++++++++++++++ pcapkit/protocols/transport/tcp.py | 4 +- tests/project/test_all_layer_grouping_unit.py | 6 +- ...test_no_moved_name_re_exports_1744_unit.py | 4 ++ .../test_rfc_constants_one_home_1739_unit.py | 2 +- tests/protocols/application/test_ftp_unit.py | 2 +- .../application/test_layer_placement_unit.py | 54 +++++++++++++++- 17 files changed, 188 insertions(+), 43 deletions(-) create mode 100644 docs/source/pcapkit/protocols/application/ftp_data.rst create mode 100644 pcapkit/protocols/application/ftp_data.py diff --git a/docs/source/contributing/conventions/protocol-layer-placement.rst b/docs/source/contributing/conventions/protocol-layer-placement.rst index af955e2ba6..43d5ff1573 100644 --- a/docs/source/contributing/conventions/protocol-layer-placement.rst +++ b/docs/source/contributing/conventions/protocol-layer-placement.rst @@ -140,8 +140,8 @@ the MRO, so whichever base owns it *earliest* wins: :obj:`None` from :class:`~pcapkit.protocols.protocol.ProtocolBase` -- so a class naming both :class:`~pcapkit.protocols.application.application.Application` and :class:`~pcapkit.protocols.misc.raw.Raw` reports ``'Application'`` in either base order. -:class:`~pcapkit.protocols.application.ftp.FTP_DATA` names -:class:`~pcapkit.protocols.misc.raw.Raw` alone, so its ``__layer__`` is :obj:`None`. +So :class:`~pcapkit.protocols.application.ftp_data.FTP_DATA`, declared +``class FTP_DATA(Application, Raw)``, reports ``'Application'``. :class:`~pcapkit.protocols.link.arp.ARP`'s chain reaches :class:`~pcapkit.protocols.link.link.Link`, which **does** own it, so ``class RARP(ARP, Application)`` would report ``'Link'`` and be wrong. diff --git a/docs/source/contributing/pep.rst b/docs/source/contributing/pep.rst index 597b430bb1..d474c2aad4 100644 --- a/docs/source/contributing/pep.rst +++ b/docs/source/contributing/pep.rst @@ -301,7 +301,7 @@ Some wanted only a table entry, because the dissector was already there: :class:`~pcapkit.protocols.link.l2tpv2.L2TPv2` at UDP port 1701. ``__index__`` raises on both, which is correct: neither is reached through a link-layer EtherType. -* **Done.** :class:`~pcapkit.protocols.application.ftp.FTP_DATA` is bound at TCP +* **Done.** :class:`~pcapkit.protocols.application.ftp_data.FTP_DATA` is bound at TCP port 20 (IANA ``ftp-data``). It is a thin :class:`~pcapkit.protocols.misc.raw.Raw` subclass, so this buys the payload a *name* rather than a parse -- which is the right answer for a data channel diff --git a/docs/source/ext.rst b/docs/source/ext.rst index f87e939f4a..b7bd0ffba4 100644 --- a/docs/source/ext.rst +++ b/docs/source/ext.rst @@ -74,7 +74,7 @@ The protocol classes :mod:`pcapkit` ships: +------------------------------------------------------------------+----------------+-----------------------+-------------------------------------------------------------+ | | | :class:`pcapkit.protocols.application.ftp.FTP` | + + FTP Family +-----------------------+-------------------------------------------------------------+ -| | | :class:`pcapkit.protocols.application.ftp.FTP_DATA` | +| | | :class:`pcapkit.protocols.application.ftp_data.FTP_DATA` | + Application Layer +----------------+-----------------------+-------------------------------------------------------------+ | (:class:`~pcapkit.protocols.application.application.Application` | | :class:`pcapkit.protocols.application.http.HTTP` | + subclasses) + +-----------------------+-------------------------------------------------------------+ diff --git a/docs/source/pcapkit/protocols/application/ftp.rst b/docs/source/pcapkit/protocols/application/ftp.rst index 720a294511..b2b720d55b 100644 --- a/docs/source/pcapkit/protocols/application/ftp.rst +++ b/docs/source/pcapkit/protocols/application/ftp.rst @@ -6,8 +6,9 @@ FTP - File Transfer Protocol :mod:`pcapkit.protocols.application.ftp` contains :class:`~pcapkit.protocols.application.ftp.FTP`, which implements an extractor for File Transfer Protocol -(FTP) [*]_, and :class:`~pcapkit.protocols.application.ftp.FTP_DATA` -for its data channel. +(FTP) [*]_, the control channel; the data channel is +:class:`~pcapkit.protocols.application.ftp_data.FTP_DATA`, in +:mod:`pcapkit.protocols.application.ftp_data`. .. versionchanged:: 1.5.0 @@ -17,6 +18,13 @@ for its data channel. parse and rebuild as captured (:issue:`1238`, :issue:`1239`, :issue:`1240`, :issue:`1241`, :issue:`1303`, :issue:`1333`). +.. versionremoved:: 1.5.0 + + ``pcapkit.protocols.application.ftp.FTP_DATA``, with no alias. The class + moved to :mod:`pcapkit.protocols.application.ftp_data`, since TCP port 20 + dispatches it and port 21 the control channel -- two protocols, so two + modules (:issue:`2285`). + .. autoclass:: pcapkit.protocols.application.ftp.FTP :no-members: :show-inheritance: @@ -29,12 +37,6 @@ for its data channel. .. automethod:: _make_data -.. autoclass:: pcapkit.protocols.application.ftp.FTP_DATA - :no-members: - :show-inheritance: - - .. autoproperty:: name - Auxiliary Data -------------- diff --git a/docs/source/pcapkit/protocols/application/ftp_data.rst b/docs/source/pcapkit/protocols/application/ftp_data.rst new file mode 100644 index 0000000000..2288d5083a --- /dev/null +++ b/docs/source/pcapkit/protocols/application/ftp_data.rst @@ -0,0 +1,29 @@ +FTP_DATA - FTP Data Channel +=========================== + +.. module:: pcapkit.protocols.application.ftp_data + +:mod:`pcapkit.protocols.application.ftp_data` contains +:class:`~pcapkit.protocols.application.ftp_data.FTP_DATA` only, +which implements an extractor for the data channel of File +Transfer Protocol (FTP) [*]_, whose octets it keeps undissected. + +.. versionadded:: 1.5.0 + + Moved here from :mod:`pcapkit.protocols.application.ftp`, which keeps + :class:`~pcapkit.protocols.application.ftp.FTP` alone. The control and data + channels are dispatched separately -- FTP at TCP port 21, the data channel + at port 20 -- so each is a protocol of its own and gets its own module, + unlike :class:`~pcapkit.protocols.application.rarp.RARP` and + :class:`~pcapkit.protocols.application.rarp.DRARP`, which one dispatch + entry reaches (:issue:`2285`). + +.. autoclass:: pcapkit.protocols.application.ftp_data.FTP_DATA + :no-members: + :show-inheritance: + + .. autoproperty:: name + +.. rubric:: Footnotes + +.. [*] https://en.wikipedia.org/wiki/File_Transfer_Protocol diff --git a/docs/source/pcapkit/protocols/application/index.rst b/docs/source/pcapkit/protocols/application/index.rst index 5561a41080..1c22c570d7 100644 --- a/docs/source/pcapkit/protocols/application/index.rst +++ b/docs/source/pcapkit/protocols/application/index.rst @@ -18,6 +18,7 @@ function rather than encapsulation, see httpv1 httpv2 ftp + ftp_data ngap ospf rarp diff --git a/docs/source/pcapkit/protocols/index.rst b/docs/source/pcapkit/protocols/index.rst index 855de5a812..bbf9c04857 100644 --- a/docs/source/pcapkit/protocols/index.rst +++ b/docs/source/pcapkit/protocols/index.rst @@ -42,7 +42,7 @@ from given information. The class hierarchy of :mod:`pcapkit.protocols`: end subgraph application [Application Layer] - Application --> HTTP & FTP & NGAP & OSPF & RARP + Application --> HTTP & FTP & FTP_DATA & NGAP & OSPF & RARP subgraph http [HTTP Family] HTTP --> h1["HTTP/1.*"] & h2["HTTP/2"] @@ -145,7 +145,7 @@ from given information. The class hierarchy of :mod:`pcapkit.protocols`: click h1 "application/httpv1.html#pcapkit.protocols.application.httpv1.HTTP" click h2 "application/httpv2.html#pcapkit.protocols.application.httpv2.HTTP" click FTP "application/ftp.html#pcapkit.protocols.application.ftp.FTP" - click FTP_DATA "application/ftp.html#pcapkit.protocols.application.ftp.FTP_DATA" + click FTP_DATA "application/ftp_data.html#pcapkit.protocols.application.ftp_data.FTP_DATA" click NGAP "application/ngap.html#pcapkit.protocols.application.ngap.NGAP" click OSPF "application/ospf.html#pcapkit.protocols.application.ospf.OSPF" click RARP "application/rarp.html#pcapkit.protocols.application.rarp.RARP" diff --git a/examples/generators/dispatch.py b/examples/generators/dispatch.py index cfc8037733..ac1d0af029 100644 --- a/examples/generators/dispatch.py +++ b/examples/generators/dispatch.py @@ -657,7 +657,7 @@ def _link_enum() -> 'Any': 'internet/Shim6': ('pcapkit.protocols.internet.ipv6_ext', 'IPv6_Ext'), # -- TCP.__proto__ (port) -------------------------------------------------- - 'tcp/20': ('pcapkit.protocols.application.ftp', 'FTP_DATA'), + 'tcp/20': ('pcapkit.protocols.application.ftp_data', 'FTP_DATA'), 'tcp/21': ('pcapkit.protocols.application.ftp', 'FTP'), # The version-identifying proxy (#682), as for ``udp/80`` and ``udp/8080`` # below. diff --git a/pcapkit/protocols/application/__init__.py b/pcapkit/protocols/application/__init__.py index 3602b93445..2416e88cc9 100644 --- a/pcapkit/protocols/application/__init__.py +++ b/pcapkit/protocols/application/__init__.py @@ -21,7 +21,8 @@ from pcapkit.protocols.application.application import Application # Utility Classes for Protocols -from pcapkit.protocols.application.ftp import FTP, FTP_DATA +from pcapkit.protocols.application.ftp import FTP +from pcapkit.protocols.application.ftp_data import FTP_DATA from pcapkit.protocols.application.httpv1 import HTTP as HTTPv1 from pcapkit.protocols.application.httpv2 import HTTP as HTTPv2 from pcapkit.protocols.application.ngap import NGAP diff --git a/pcapkit/protocols/application/ftp.py b/pcapkit/protocols/application/ftp.py index 2a8df6fc22..a4a801032e 100644 --- a/pcapkit/protocols/application/ftp.py +++ b/pcapkit/protocols/application/ftp.py @@ -7,9 +7,9 @@ :mod:`pcapkit.protocols.application.ftp` contains :class:`~pcapkit.protocols.application.ftp.FTP`, which implements an extractor for File Transfer Protocol -(FTP) [*]_, and -:class:`~pcapkit.protocols.application.ftp.FTP_DATA` -for its data channel. +(FTP) [*]_, the control channel; the data channel is +:class:`~pcapkit.protocols.application.ftp_data.FTP_DATA`, in +:mod:`pcapkit.protocols.application.ftp_data`. .. [*] https://en.wikipedia.org/wiki/File_Transfer_Protocol @@ -21,6 +21,13 @@ parse and rebuild as captured (:issue:`1238`, :issue:`1239`, :issue:`1240`, :issue:`1241`, :issue:`1303`, :issue:`1333`). +.. versionremoved:: 1.5.0 + + ``pcapkit.protocols.application.ftp.FTP_DATA``, with no alias. The class + moved to :mod:`pcapkit.protocols.application.ftp_data`, since TCP port 20 + dispatches it and port 21 the control channel -- two protocols, so two + modules (:issue:`2285`). + """ import re from typing import TYPE_CHECKING @@ -32,7 +39,6 @@ from pcapkit.protocols.data.application.ftp import FTP as Data_FTP from pcapkit.protocols.data.application.ftp import Request as Data_Request from pcapkit.protocols.data.application.ftp import Response as Data_Response -from pcapkit.protocols.misc.raw import Raw from pcapkit.protocols.schema.application.ftp import FTP as Schema_FTP from pcapkit.utilities.chardet import detect from pcapkit.utilities.compat import StrEnum, auto @@ -43,7 +49,7 @@ from typing_extensions import Literal -__all__ = ['FTP', 'FTP_DATA'] +__all__ = ['FTP'] # regex for FTP, per :rfc:`959#section-4` (replies) and :rfc:`959#section-5` # (commands): a command is `` [ ] ``, a reply line is @@ -322,17 +328,3 @@ def _decode_args(self, args: 'Optional[bytes]') -> 'tuple[Optional[str], Optiona except (LookupError, UnicodeError): pass return args.decode('latin-1'), 'latin-1' - - -class FTP_DATA(Raw): - """This class implements FTP data channel transmission.""" - - ########################################################################## - # Properties. - ########################################################################## - - # name of current protocol - @property - def name(self) -> 'Literal["FTP_DATA"]': # type: ignore[override] - """Name of current protocol.""" - return 'FTP_DATA' diff --git a/pcapkit/protocols/application/ftp_data.py b/pcapkit/protocols/application/ftp_data.py new file mode 100644 index 0000000000..60ae65bcbe --- /dev/null +++ b/pcapkit/protocols/application/ftp_data.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +"""FTP_DATA - FTP Data Channel +================================= + +.. module:: pcapkit.protocols.application.ftp_data + +:mod:`pcapkit.protocols.application.ftp_data` contains +:class:`~pcapkit.protocols.application.ftp_data.FTP_DATA` only, +which implements an extractor for the data channel of File +Transfer Protocol (FTP) [*]_, whose octets it keeps undissected. + +.. [*] https://en.wikipedia.org/wiki/File_Transfer_Protocol + +.. versionadded:: 1.5.0 + + Moved here from :mod:`pcapkit.protocols.application.ftp`, which keeps + :class:`~pcapkit.protocols.application.ftp.FTP` alone. The control and data + channels are dispatched separately -- FTP at TCP port 21, the data channel + at port 20 -- so each is a protocol of its own and gets its own module, + unlike :class:`~pcapkit.protocols.application.rarp.RARP` and + :class:`~pcapkit.protocols.application.rarp.DRARP`, which one dispatch + entry reaches (:issue:`2285`). + +""" +from typing import TYPE_CHECKING + +from pcapkit.protocols.application.application import Application +from pcapkit.protocols.misc.raw import Raw + +if TYPE_CHECKING: + from typing_extensions import Literal + +__all__ = ['FTP_DATA'] + + +class FTP_DATA(Application, Raw): # pylint: disable=abstract-method + """This class implements FTP data channel transmission. + + The layer base :class:`~pcapkit.protocols.application.application.Application` + comes first, for ``layer == 'Application'``, then the parsing base + :class:`~pcapkit.protocols.misc.raw.Raw`, which keeps the channel's octets + undissected. See :doc:`/contributing/conventions/protocol-layer-placement`. + + .. versionchanged:: 1.5.0 + + Derives from :class:`~pcapkit.protocols.application.application.Application` + as well as :class:`~pcapkit.protocols.misc.raw.Raw`, so its layer is + ``'Application'``, where it was :obj:`None`, and its ``__index__`` is + :meth:`Application.__index__ + `, + raising :exc:`~pcapkit.utilities.exceptions.IntError` where it raised + :exc:`~pcapkit.utilities.exceptions.UnsupportedCall` (:issue:`2285`). + + """ + + ########################################################################## + # Properties. + ########################################################################## + + # name of current protocol + @property + def name(self) -> 'Literal["FTP_DATA"]': # type: ignore[override] + """Name of current protocol.""" + return 'FTP_DATA' diff --git a/pcapkit/protocols/transport/tcp.py b/pcapkit/protocols/transport/tcp.py index 51bf127972..19ee910e77 100644 --- a/pcapkit/protocols/transport/tcp.py +++ b/pcapkit/protocols/transport/tcp.py @@ -182,7 +182,7 @@ class TCP(Transport[Data_TCP, Schema_TCP], * - Port Number - Protocol * - 20 - - :class:`pcapkit.protocols.application.ftp.FTP_DATA` + - :class:`pcapkit.protocols.application.ftp_data.FTP_DATA` * - 21 - :class:`pcapkit.protocols.application.ftp.FTP` * - 80 @@ -362,7 +362,7 @@ class TCP(Transport[Data_TCP, Schema_TCP], # connection preface, then an HTTP/1 start line) and trial-parses # only as a last resort. UDP's table binds the same class for the # same ports. - 20: ModuleDescriptor('pcapkit.protocols.application.ftp', 'FTP_DATA'), + 20: ModuleDescriptor('pcapkit.protocols.application.ftp_data', 'FTP_DATA'), 21: ModuleDescriptor('pcapkit.protocols.application.ftp', 'FTP'), 80: ModuleDescriptor('pcapkit.protocols.application.http', 'HTTP'), 8080: ModuleDescriptor('pcapkit.protocols.application.http', 'HTTP'), diff --git a/tests/project/test_all_layer_grouping_unit.py b/tests/project/test_all_layer_grouping_unit.py index 4a0ea1056d..f6c29f8048 100644 --- a/tests/project/test_all_layer_grouping_unit.py +++ b/tests/project/test_all_layer_grouping_unit.py @@ -76,11 +76,11 @@ def classes_on(self, layer: 'str') -> 'set[str]': def foreign(self, group: 'list[str]', layer: 'str') -> 'set[str]': """Names in ``group`` whose class reports a layer other than ``layer``. - A class whose ``__layer__`` is unset (``FTP_DATA`` subclasses ``Raw``) is not - foreign. + An unset ``__layer__`` is foreign too: a class that names no layer base + reports :obj:`None` and would otherwise pass unnoticed (#2285). """ return {name for name in group - if (getattr(self.exported.get(name), '__layer__', None) or layer) != layer} + if getattr(self.exported.get(name), '__layer__', None) != layer} def test_groups_are_found(self) -> None: self.assertIn('ARP', self.link) diff --git a/tests/project/test_no_moved_name_re_exports_1744_unit.py b/tests/project/test_no_moved_name_re_exports_1744_unit.py index d97c357893..475dbcb4ad 100644 --- a/tests/project/test_no_moved_name_re_exports_1744_unit.py +++ b/tests/project/test_no_moved_name_re_exports_1744_unit.py @@ -54,6 +54,7 @@ ('pcapkit.foundation.reassembly.data', 'DeferredPacket'), ('pcapkit.foundation.reassembly.data.data', 'DeferredPacket'), ('pcapkit.foundation.traceflow.data.data', 'DeferredPacket'), + ('pcapkit.protocols.application.ftp', 'FTP_DATA'), ('pcapkit.protocols.application.ngap', 'ProcedureCode'), ('pcapkit.protocols.application.ngap', 'ProtocolIE'), ('pcapkit.protocols.internet.mh', 'NTPTimestamp'), @@ -80,6 +81,9 @@ 'pcapkit/corekit/fields/field.py', 'pcapkit/corekit/fields/field.rst'), ('pcapkit.protocols.internet.mh', 'NTPTimestamp', 'pcapkit/protocols/internet/mh.py', 'pcapkit/protocols/internet/mh.rst'), + ('pcapkit.protocols.application.ftp', 'FTP_DATA', + 'pcapkit/protocols/application/ftp.py', + 'pcapkit/protocols/application/ftp.rst'), ) 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 5dfa4bd9dd..e58b539cb8 100644 --- a/tests/project/test_rfc_constants_one_home_1739_unit.py +++ b/tests/project/test_rfc_constants_one_home_1739_unit.py @@ -291,7 +291,7 @@ ('pcapkit/protocols/transport/tcp.py', 'bytes(20)'): 'an MP_JOIN SYN/ACK HMAC default of 20 octets', ('pcapkit/protocols/transport/tcp.py', - "{20: ModuleDescriptor('pcapkit.protocols.application.ftp', 'FTP_DATA')}"): + "{20: ModuleDescriptor('pcapkit.protocols.application.ftp_data', 'FTP_DATA')}"): 'the FTP data port, not the minimum header length', ('pcapkit/protocols/transport/tcp.py', 'schema.length not in (4, 12, 20, 22, 24)'): 'the MP_CAPABLE option lengths, :rfc:`8684#section-3.1`', diff --git a/tests/protocols/application/test_ftp_unit.py b/tests/protocols/application/test_ftp_unit.py index 37dee6535e..eb20d758ef 100644 --- a/tests/protocols/application/test_ftp_unit.py +++ b/tests/protocols/application/test_ftp_unit.py @@ -237,7 +237,7 @@ def test_ftp_make_data_preserves_request_and_response_fields(self) -> None: ) def test_ftp_data_channel_name_is_stable(self) -> None: - from pcapkit.protocols.application.ftp import FTP_DATA + from pcapkit.protocols.application.ftp_data import FTP_DATA ftp_data = object.__new__(FTP_DATA) diff --git a/tests/protocols/application/test_layer_placement_unit.py b/tests/protocols/application/test_layer_placement_unit.py index ac578e5f70..d9acd264bc 100644 --- a/tests/protocols/application/test_layer_placement_unit.py +++ b/tests/protocols/application/test_layer_placement_unit.py @@ -7,7 +7,8 @@ §7.2.2 and confines the internet layer to IP, ICMP and IGMP at §4.1; :rfc:`1122#section-1.1.3` lists RARP in the application layer while ARP sits in the Link Layer chapter at §2.3.2. The rule and its citations are written down at -:doc:`/contributing/conventions/protocol-layer-placement`. +:doc:`/contributing/conventions/protocol-layer-placement`. ``FTP_DATA`` follows the +same convention with ``Raw`` as its parsing base (#2285). These are pinned separately, because each can break independently: @@ -53,6 +54,11 @@ def rarp_request() -> bytes: '66778899aabb' '0a000001') +def tcp(sport: 'int', dport: 'int', payload: bytes = b'') -> bytes: + """Wrap ``payload`` in a minimal 20-octet TCP header.""" + return struct.pack('!HHIIBBHHH', sport, dport, 0, 0, 0x50, 0x18, 8192, 0, 0) + payload + + def ipv4(proto: 'int', payload: bytes) -> bytes: """A minimal IPv4 header carrying ``payload`` under protocol ``proto``.""" total = 20 + len(payload) @@ -128,6 +134,33 @@ def test_rarp_subclasses_both_application_and_arp_in_that_order(self) -> None: # DRARP inherits the pair unchanged self.assertEqual(DRARP.__bases__, (RARP,)) + def test_ftp_data_subclasses_both_application_and_raw_in_that_order(self) -> None: + """``FTP_DATA`` follows the convention: layer base first, then ``Raw`` (#2285).""" + from pcapkit.protocols.application.application import Application + from pcapkit.protocols.application.ftp_data import FTP_DATA + from pcapkit.protocols.misc.raw import Raw + + self.assertEqual(FTP_DATA.__bases__, (Application, Raw)) + self.assertEqual(FTP_DATA.__layer__, 'Application') + self.assertEqual(FTP_DATA(b'binary-file-contents').layer, 'Application') + + def test_ftp_data_still_parses_and_rebuilds_as_raw(self) -> None: + """The ``Application`` base leaves ``Raw``'s parsing and rebuild untouched.""" + from pcapkit.protocols.application.ftp_data import FTP_DATA + from pcapkit.protocols.misc.null import NoPayload + from pcapkit.utilities.exceptions import IntError + + packet = FTP_DATA(b'binary-file-contents') + self.assertEqual(packet.info.packet, b'binary-file-contents') + self.assertEqual(str(packet.protochain), 'FTP_DATA') + self.assertIsInstance(packet.payload, NoPayload) + self.assertEqual(bytes(FTP_DATA.from_data(packet.info)), b'binary-file-contents') + self.assertEqual(FTP_DATA(packet=b'abc').data, b'abc') + + # reached by TCP port 20, a port is no index: ``Application``'s refusal + with self.assertRaises(IntError): + FTP_DATA.__index__() + def test_link_owns_layer_but_raw_does_not(self) -> None: """The mechanism the ordering convention rests on, asserted directly.""" from pcapkit.protocols.link.link import Link @@ -343,6 +376,25 @@ def test_rarp_over_ethernet_is_unchanged_under_every_layer_limit(self) -> None: self.addCleanup(extraction.__del__) self.assertEqual(str(extraction.frame[0].protochain), chain) + def test_layer_application_extraction_terminates_at_ftp_data(self) -> None: + """``layer='application'`` and ``layer=FTP_DATA`` both stop at FTP_DATA (#2285).""" + import pcapkit + from pcapkit.const.reg.linktype import LinkType + from pcapkit.protocols.application.ftp_data import FTP_DATA + + path = make_pcap(LinkType.IPV4, ipv4(6, tcp(50000, 20, b'binary-file-contents'))) + for layer in ('application', FTP_DATA): + with self.subTest(layer=layer): + extraction = pcapkit.extract(fin=path, nofile=True, store=True, + engine='pcapkit', layer=layer) + self.addCleanup(extraction.__del__) + frame = extraction.frame[0] + self.assertEqual(str(frame.protochain), 'IPv4:TCP:FTP_DATA') + + ftp_data = frame.payload.payload.payload + self.assertIsInstance(ftp_data, FTP_DATA) + self.assertTrue(ftp_data._sigterm, "'application' must terminate at FTP_DATA") + def test_unlimited_extraction_is_unchanged(self) -> None: frame = self.extract_ipv4_ospf() self.assertEqual(str(frame.protochain), 'IPv4:OSPFv2:Raw')