class Parser

A parser for multipart data based on RFC 2046 and RFC 2387. Parses multipart bodies and provides an enumerable interface to access the parts.

Nested Classes and Modules

class Part

Represents a single part within a multipart message.

Definitions

PREAMBLE_SIZE_LIMIT = 64 * 1024

The preamble size limit.

HEADER_SIZE_LIMIT = 64 * 1024

The header size limit for each part.

HEADER_COUNT_LIMIT = 64

The header count limit for each part.

PART_COUNT_LIMIT = 128

The part count limit.

def initialize(readable, boundary, preamble_size_limit: PREAMBLE_SIZE_LIMIT, header_size_limit: HEADER_SIZE_LIMIT, header_count_limit: HEADER_COUNT_LIMIT, part_count_limit: PART_COUNT_LIMIT)

Initialize a new multipart parser.

Signature

parameter readable IO, IO::Stream

The readable stream containing multipart data.

parameter boundary String

The boundary string that separates the parts.

parameter preamble_size_limit Integer | Nil

The preamble size limit, or nil for no limit.

parameter header_size_limit Integer | Nil

The header size limit per part, or nil for no limit.

parameter header_count_limit Integer | Nil

The header count limit per part, or nil for no limit.

parameter part_count_limit Integer | Nil

The part count limit, or nil for no limit.

Implementation

def initialize(readable, boundary, preamble_size_limit: PREAMBLE_SIZE_LIMIT, header_size_limit: HEADER_SIZE_LIMIT, header_count_limit: HEADER_COUNT_LIMIT, part_count_limit: PART_COUNT_LIMIT)
	limits = [preamble_size_limit, header_size_limit, header_count_limit, part_count_limit]
	
	if limits.any?{|limit| limit and limit < 0}
		raise ArgumentError, "Multipart limits must be non-negative!"
	end
	
	@readable = IO::Stream(readable)
	@boundary = boundary
	@preamble_size_limit = preamble_size_limit
	@header_size_limit = header_size_limit
	@header_count_limit = header_count_limit
	@part_count_limit = part_count_limit
	
	@boundary_marker = "--#{@boundary}\r\n".freeze
end

def each

Enumerate through each part in the multipart data. Yields each part for processing. If no block is given, returns an enumerator.

Signature

returns Enumerator, Boolean

An enumerator if no block given, or true when complete.

Implementation

def each
	return to_enum unless block_given?
	
	preamble_size = 0
	
	# Read lines until we find the first boundary:
	while true
		if line = read_line(preamble_size, @preamble_size_limit, allowance: @boundary_marker.bytesize, chomp: false)
			if line == @boundary_marker
				break
			else
				preamble_size += line.bytesize
				check_limit(:preamble_size, preamble_size, @preamble_size_limit)
			end
		else
			# End of stream reached without finding boundary:
			raise EOFError, "No multipart boundary found in stream!"
		end
	end
	
	part_count = 0
	
	while true
		part_count += 1
		check_limit(:part_count, part_count, @part_count_limit)
		
		part = read_part
		break unless part
		
		if part.read_empty_boundary?
		else
			yield part
			
			# Advance to the next boundary after the consumer returns normally. If the consumer raises, stop parsing without draining the request body.
			part.discard
		end
		
		# Check if this was the last part:
		break if part.closing_boundary?
	end
	
	return true
end