I've asked Code Climate to generate metrics for the ftpd Ruby gem. It correctly identified the God class; I know what to do about that. But one of the smaller classes has me stumped. This is telnet.rb:
# -*- ruby encoding: us-ascii -*-
module Ftpd
# Handle the limited processing of Telnet sequences required by the
# FTP RFCs.
#
# Telnet option processing is quite complex, but we need do only a
# simple subset of it, since we can disagree with any request by the
# client to turn on an option (RFC-1123 4.1.2.12). Adhering to
# RFC-1143 ("The Q Method of Implementing TELNET Option Negiation"),
# and supporting only what's needed to keep all options turned off:
#
# * Reply to WILL sequence with DONT sequence
# * Reply to DO sequence with WONT sequence
# * Ignore WONT sequence
# * Ignore DONT sequence
#
# We also handle the "interrupt process" and "data mark" sequences,
# which the client sends before the ABORT command, by ignoring them.
#
# All Telnet sequence start with an IAC, followed by at least one
# character. Here are the sequences we care about:
#
# SEQUENCE CODES
# ----------------- --------------------
# WILL IAC WILL option-code
# WONT IAC WONT option-code
# DO IAC DO option-code
# DONT IAC DONT option-code
# escaped 255 IAC IAC
# interrupt process IAC IP
# data mark IAC DM
#
# Any pathalogical sequence (e.g. IAC + \x01), or any sequence we
# don't recognize, we pass through.
class Telnet
# The command with recognized Telnet sequences removed
attr_reader :plain
# Any Telnet sequences to send
attr_reader :reply
# Create a new instance with a command that may contain Telnet
# sequences.
# @param command [String]
def initialize(command)
telnet_state_machine command
end
private
module Codes
IAC = 255.chr # 0xff
DONT = 254.chr # 0xfe
DO = 253.chr # 0xfd
WONT = 252.chr # 0xfc
WILL = 251.chr # 0xfb
IP = 244.chr # 0xf4
DM = 242.chr # 0xf2
end
include Codes
def telnet_state_machine (command)
@plain = ''
@reply = ''
state = :idle
command.each_char do |c|
case state
when :idle
if c == IAC
state = :iac
else
@plain << c
end
when :iac
case c
when IAC
@plain << c
state = :idle
when WILL
state = :will
when WONT
state = :wont
when DO
state = :do
when DONT
state = :dont
when IP
state = :idle
when DM
state = :idle
else
@plain << IAC + c
state = :idle
end
when :will
@reply << IAC + DONT + c
state = :idle
when :wont
state = :idle
when :do
@reply << IAC + WONT + c
state = :idle
when :dont
state = :idle
else
raise "Unknown state #{state.inspect}"
end
end
end
end
end
Code Climate does not like the complexity of #telnet_state_machine
. I agree, but I don't know how to reduce the complexity without also making the state machine harder to follow. State machines never seem to be all that readable as it is. What would you suggest?
Note: If you want to try some refactoring, this class has rspec test coverage. Just do "git clone" of the ftpd project.