# frozen_string_literal: true # Released under the MIT License. # Copyright, 2019-2024, by Samuel Williams. # Copyright, 2023, by Thomas Morgan. require 'protocol/http/body/readable' module Protocol module HTTP1 module Body class Chunked < HTTP::Body::Readable CRLF = "\r\n" def initialize(stream, headers) @stream = stream @finished = false @headers = headers @length = 0 @count = 0 end def empty? @stream.nil? end def close(error = nil) if @stream # We only close the connection if we haven't completed reading the entire body: unless @finished @stream.close_read end @stream = nil end super end VALID_CHUNK_LENGTH = /\A[0-9a-fA-F]+\z/ # Follows the procedure outlined in https://tools.ietf.org/html/rfc7230#section-4.1.3 def read if !@finished if @stream length, _extensions = 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 stream is now closed: @stream = nil @finished = true return nil end # Read trailing CRLF: chunk = @stream.read(length + 2) # ...and chomp it off: chunk.chomp!(CRLF) @length += length @count += 1 return chunk end # If the stream has been closed before we have read the final chunk, raise an error: raise EOFError, "Stream closed before expected length was read!" end end def inspect "\#<#{self.class} #{@length} bytes read in #{@count} chunks>" end private def read_line? @stream.gets(CRLF, chomp: true) end def read_line read_line? or raise EOFError end def read_trailer while line = 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 end end end end