class Protocol::HTTP1::Body::Chunked

See tools.ietf.org/html/rfc7230#section-4.1 for more details on the chunked transfer encoding.
Represents a chunked body, which is a series of chunks, each with a length prefix.

def as_json(...)

@returns [Hash] JSON representation for tracing and debugging.
def as_json(...)
	super.merge(
		count: @count,
		finished: @finished,
		state: @connection ? "open" : "closed"
	)
end

def close(error = nil)

@parameter error [Exception | Nil] the error that caused the body to be closed, if any.

Close the connection and mark the body as finished.
def close(error = nil)
	if connection = @connection
		@connection = nil
		
		unless @finished
			connection.close_read
		end
	end
	
	super
end

def empty?

@returns [Boolean] true if the body is empty, in other words {read} will return `nil`.
def empty?
	@connection.nil?
end

def initialize(connection, headers)

@parameter headers [Protocol::HTTP::Headers] the headers to read the trailer into, if any.
@parameter connection [Protocol::HTTP1::Connection] the connection to read the body from.

Initialize the chunked body.
def initialize(connection, headers)
	@connection = connection
	@finished = false
	
	@headers = headers
	
	@length = 0
	@count = 0
end

def inspect

@returns [String] a human-readable representation of the body.
def inspect
	"\#<#{self.class} #{@length} bytes read in #{@count} chunks, #{@finished ? 'finished' : 'reading'}>"
end

def length

@attribute [Integer] the length of the body if known.
def length
	# We only know the length once we've read the final chunk:
	if @finished
		@length
	end
end

def read

@raises [EOFError] if the connection is closed before the expected length is read.
@returns [String | Nil] the next chunk of data, or `nil` if the body is finished.

Follows the procedure outlined in https://tools.ietf.org/html/rfc7230#section-4.1.3

Read a chunk of data.
def read
	if !@finished
		if @connection
			length, _extensions = @connection.read_line.split(";", 2)
			
			unless length =~ VALID_CHUNK_LENGTH
				raise BadRequest, "Invalid chunk length: #{length.inspect}"
			end
			
			# It is possible this line contains chunk extension, so we use `to_i` to only consider the initial integral part:
			length = Integer(length, 16)
			
			if length == 0
				read_trailer
				
				# The final chunk has been read and the connection is now closed:
				@connection.receive_end_stream!
				@connection = nil
				@finished = true
				
				return nil
			end
			
			# Read trailing CRLF:
			chunk = @connection.read(length + 2)
			
			if chunk.bytesize == length + 2
				# ...and chomp it off:
				chunk.chomp!(CRLF)
				
				@length += length
				@count += 1
				
				return chunk
			else
				# The connection has been closed before we have read the requested length:
				@connection.close_read
				@connection = nil
			end
		end
		
		# If the connection has been closed before we have read the final chunk, raise an error:
		raise EOFError, "connection closed before expected length was read!"
	end
end

def read_trailer

Read the trailer from the connection, and add any headers to the trailer.
def read_trailer
	while line = @connection.read_line?
		# Empty line indicates end of trailer:
		break if line.empty?
		
		if match = line.match(HEADER)
			@headers.add(match[1], match[2])
		else
			raise BadHeader, "Could not parse header: #{line.inspect}"
		end
	end
end