Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions pcapkit/protocols/application/ftp.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_FTP':
return ftp

def make(self,
cmmd: 'Optional[Enum_Command | str | bytes]' = None,
cmmd: 'Optional[Enum_Command | str | bytes | bytearray]' = None,
code: 'Optional[Enum_ReturnCode | int | str | bytes]' = None,
args: 'Optional[str | bytes]' = None,
more: 'bool' = False,
Expand All @@ -186,7 +186,9 @@ def make(self,
which keeps the case the command was read in.

Args:
cmmd: FTP command.
cmmd: FTP command, a :class:`str` (an
:class:`~pcapkit.const.ftp.command.Command` member is one),
:class:`bytes` or :class:`bytearray`.
code: FTP status code.
args: Optional FTP command arguments and/or status messages.
more: More status messages to follow for response packets.
Expand All @@ -202,17 +204,20 @@ def make(self,

Raises:
ProtocolError: If neither or both of ``cmmd`` and ``code`` are
given, or if ``lines`` are given other than as the rest of a
multi-line reply.
given, if ``cmmd`` is not a :class:`str`, :class:`bytes` or
:class:`bytearray` (a :obj:`bool` included), or if ``lines``
are given other than as the rest of a multi-line reply.

"""
if cmmd is not None and code is None:
if isinstance(cmmd, bytes):
prefix = cmmd
# NOTE: ``Command`` is a ``StrEnum``, so a member takes the ``str``
# branch; anything else is the wrong type (#2290).
if isinstance(cmmd, (bytes, bytearray)):
prefix = bytes(cmmd)
elif isinstance(cmmd, str):
prefix = cmmd.encode()
else:
prefix = cmmd.value
raise ProtocolError(f'FTP: invalid command: must be a str, bytes or bytearray, not {cmmd!r}')
if raw_cmmd is not None and raw_cmmd.upper() == prefix.upper():
prefix = raw_cmmd

Expand Down
23 changes: 21 additions & 2 deletions tests/protocols/application/test_ftp_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,15 +173,15 @@ def test_ftp_make_builds_request_and_response_packets(self) -> None:
ftp = object.__new__(FTP)
request = ftp.make(cmmd=Command.USER, args='anonymous')
request_bytes = ftp.make(cmmd=b'NOOP')
request_value = ftp.make(cmmd=SimpleNamespace(value=b'PWD'))
request_bytearray = ftp.make(cmmd=bytearray(b'PWD'))
response = ftp.make(code=ReturnCode.CODE_220, args='Ready', more=True)
response_bytes = ftp.make(code=b'230', args=b'Logged in')

# GitHub issues #1238, #1239 and #1240: every line ends in CRLF, no
# separator is written without arguments, and none after a hyphen.
self.assertEqual(request.data, b'USER anonymous\r\n')
self.assertEqual(request_bytes.data, b'NOOP\r\n')
self.assertEqual(request_value.data, b'PWD\r\n')
self.assertEqual(request_bytearray.data, b'PWD\r\n')
self.assertEqual(response.data, b'220-Ready\r\n')
self.assertEqual(response_bytes.data, b'230 Logged in\r\n')

Expand All @@ -196,6 +196,25 @@ def test_ftp_make_rejects_ambiguous_packet_type(self) -> None:
with self.assertRaises(ProtocolError):
ftp.make(cmmd='USER', code=220)

def test_ftp_make_rejects_a_command_that_is_not_str_bytes_or_bytearray(self) -> None:
"""A command of any other type raises ``ProtocolError`` (#2290)."""
from pcapkit.protocols.application.ftp import FTP
from pcapkit.utilities.exceptions import ProtocolError

ftp = object.__new__(FTP)

for cmmd in (42, True, 1.5, SimpleNamespace(value=b'PWD')):
with self.subTest(cmmd=cmmd):
with self.assertRaises(ProtocolError) as context:
ftp.make(cmmd=cmmd, args='anon')
self.assertEqual(
str(context.exception),
f'FTP: invalid command: must be a str, bytes or bytearray, not {cmmd!r}')

# a bytearray is accepted as bytes are, and keeps the captured case
self.assertEqual(ftp.make(cmmd=bytearray(b'user'), args='anon', raw_cmmd=b'user').data,
b'user anon\r\n')

def test_ftp_make_data_preserves_request_and_response_fields(self) -> None:
from pcapkit.const.ftp.command import Command
from pcapkit.const.ftp.return_code import ReturnCode
Expand Down
Loading