Module: Protocol::HTTP::Body::Stream::Reader

Included in:
Protocol::HTTP::Body::Stream
Defined in:
lib/protocol/http/body/stream.rb

Overview

This provides a read-only interface for data, which is surprisingly tricky to implement correctly.

Instance Method Summary collapse

Instance Method Details

#each(&block) ⇒ Object

Iterate over each chunk of data from the input stream.



146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/protocol/http/body/stream.rb', line 146

def each(&block)
	return to_enum unless block_given?
	
	if @buffer
		yield @buffer
		@buffer = nil
	end
	
	while chunk = read_next
		yield chunk
	end
end

#gets(separator = NEWLINE, limit = nil, chomp: false) ⇒ Object

Read a single line from the stream.



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/protocol/http/body/stream.rb', line 226

def gets(separator = NEWLINE, limit = nil, chomp: false)
	# If the separator is an integer, it is actually the limit:
	if separator.is_a?(Integer)
		limit = separator
		separator = NEWLINE
	end
	
	# If no separator is given, this is the same as a read operation:
	if separator.nil?
		# I tried using `read(limit)` here but it will block until the limit is reached, which is not usually desirable behaviour.
		return read_partial(limit)
	end
	
	# We don't want to split on the separator, so we subtract the size of the separator:
	split_offset = separator.bytesize - 1
	
	@buffer ||= read_next
	return nil if @buffer.nil?
	
	offset = 0
	until index = @buffer.index(separator, offset)
		offset = @buffer.bytesize - split_offset
		offset = 0 if offset < 0
		
		# If we have gone past the limit, we are done:
		if limit and offset >= limit
			@buffer.freeze
			matched = @buffer.byteslice(0, limit)
			@buffer = @buffer.byteslice(limit, @buffer.bytesize)
			return matched
		end
		
		# Read more data:
		if chunk = read_next
			@buffer << chunk
		else
			# No more data could be read, return the remaining data:
			buffer = @buffer
			@buffer = nil
			
			return @buffer
		end
	end
	
	# Freeze the buffer, as this enables us to use byteslice without generating a hidden copy:
	@buffer.freeze
	
	if limit and index > limit
		line = @buffer.byteslice(0, limit)
		@buffer = @buffer.byteslice(limit, @buffer.bytesize)
	else
		line = @buffer.byteslice(0, index+(chomp ? 0 : separator.bytesize))
		@buffer = @buffer.byteslice(index+separator.bytesize, @buffer.bytesize)
	end
	
	return line
end

#read(length = nil, buffer = nil) ⇒ Object

Read data from the underlying stream.

If given a non-negative length, it will read at most that many bytes from the stream. If the stream is at EOF, it will return nil.

If the length is not given, it will read all data until EOF, or return an empty string if the stream is already at EOF.

If buffer is given, then the read data will be placed into buffer instead of a newly created String object.



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/protocol/http/body/stream.rb', line 53

def read(length = nil, buffer = nil)
	return "" if length == 0
	
	buffer ||= String.new.force_encoding(Encoding::BINARY)
	
	# Take any previously buffered data and replace it into the given buffer.
	if @buffer
		buffer.replace(@buffer)
		@buffer = nil
	else
		buffer.clear
	end
	
	if length
		while buffer.bytesize < length and chunk = read_next
			buffer << chunk
		end
		
		# This ensures the subsequent `slice!` works correctly.
		buffer.force_encoding(Encoding::BINARY)
		
		# This will be at least one copy:
		@buffer = buffer.byteslice(length, buffer.bytesize)
		
		# This should be zero-copy:
		buffer.slice!(length, buffer.bytesize)
		
		if buffer.empty?
			return nil
		else
			return buffer
		end
	else
		while chunk = read_next
			buffer << chunk
		end
		
		return buffer
	end
end

#read_nonblock(length, buffer = nil, exception: nil) ⇒ Object

Read data from the stream without blocking if possible.



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/protocol/http/body/stream.rb', line 163

def read_nonblock(length, buffer = nil, exception: nil)
	@buffer ||= read_next
	chunk = nil
	
	unless @buffer
		buffer&.clear
		return
	end
	
	if @buffer.bytesize > length
		chunk = @buffer.byteslice(0, length)
		@buffer = @buffer.byteslice(length, @buffer.bytesize)
	else
		chunk = @buffer
		@buffer = nil
	end
	
	if buffer
		buffer.replace(chunk)
	else
		buffer = chunk
	end
	
	return buffer
end

#read_partial(length = nil, buffer = nil) ⇒ Object

Read some bytes from the stream.

If the length is given, at most length bytes will be read. Otherwise, one chunk of data from the underlying stream will be read.

Will avoid reading from the underlying stream if there is buffered data available.



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/protocol/http/body/stream.rb', line 101

def read_partial(length = nil, buffer = nil)
	if @buffer
		if buffer
			buffer.replace(@buffer)
		else
			buffer = @buffer
		end
		@buffer = nil
	else
		if chunk = read_next
			if buffer
				buffer.replace(chunk)
			else
				buffer = chunk
			end
		else
			buffer&.clear
			buffer = nil
		end
	end
	
	if buffer and length
		if buffer.bytesize > length
			# This ensures the subsequent `slice!` works correctly.
			buffer.force_encoding(Encoding::BINARY)

			@buffer = buffer.byteslice(length, buffer.bytesize)
			buffer.slice!(length, buffer.bytesize)
		end
	end
	
	return buffer
end

#read_until(pattern, offset = 0, chomp: false) ⇒ Object

Read data from the stream until encountering pattern.



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/protocol/http/body/stream.rb', line 195

def read_until(pattern, offset = 0, chomp: false)
	# We don't want to split on the pattern, so we subtract the size of the pattern.
	split_offset = pattern.bytesize - 1
	
	@buffer ||= read_next
	return nil if @buffer.nil?
	
	until index = @buffer.index(pattern, offset)
		offset = @buffer.bytesize - split_offset
		
		offset = 0 if offset < 0
		
		if chunk = read_next
			@buffer << chunk
		else
			return nil
		end
	end
	
	@buffer.freeze
	matched = @buffer.byteslice(0, index+(chomp ? 0 : pattern.bytesize))
	@buffer = @buffer.byteslice(index+pattern.bytesize, @buffer.bytesize)
	
	return matched
end

#readpartial(length, buffer = nil) ⇒ Object

Similar to #read_partial but raises an ‘EOFError` if the stream is at EOF.



139
140
141
# File 'lib/protocol/http/body/stream.rb', line 139

def readpartial(length, buffer = nil)
	read_partial(length, buffer) or raise EOFError, "End of file reached!"
end