lib/ftpd/stream.rb
# frozen_string_literal: true
module Ftpd
class Stream
CHUNK_SIZE = 1024 * 100 # 100kb
attr_reader :data_type
attr_reader :byte_count
# @param io [IO] The stream to read from or write to
# @param data_type [String] The FTP data type of the stream
def initialize(io, data_type)
@io, @data_type = io, data_type
@byte_count = 0
end
# Read and convert a chunk of up to CHUNK_SIZE from the stream
# @return [String] if any bytes remain to read from the stream
# @return [NilClass] if no bytes remain
def read
chunk = converted_chunk(@io)
return unless chunk
chunk = nvt_ascii_to_unix(chunk) if data_type == 'A'
record_bytes(chunk)
chunk
end
# Convert and write a chunk of up to CHUNK_SIZE to the stream from the
# provided IO object
#
# @param io [IO] The data to be written to the stream
def write(io)
while chunk = converted_chunk(io)
chunk = unix_to_nvt_ascii(chunk) if data_type == 'A'
result = @io.write(chunk)
record_bytes(chunk)
result
end
end
private
# We never want to break up any \r\n sequences in the file. To avoid
# this in an efficient way, we always pull an "extra" character from the
# stream and add it to the buffer. If the character is a \r, then we put
# it back onto the stream instead of adding it to the buffer.
def converted_chunk(io)
chunk = io.read(CHUNK_SIZE)
return unless chunk
if data_type == 'A'
next_char = io.getc
if next_char == "\r"
io.ungetc(next_char)
elsif next_char
chunk += next_char
end
end
chunk
end
def unix_to_nvt_ascii(s)
return s if s =~ /\r\n/
s.gsub(/\n/, "\r\n")
end
def nvt_ascii_to_unix(s)
s.gsub(/\r\n/, "\n")
end
def record_bytes(chunk)
@byte_count += chunk.size if chunk
end
end
end