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.



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

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.



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
283
284
285
286
287
288
# File 'lib/protocol/http/body/stream.rb', line 227

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 nil for empty buffers, otherwise return the content:
      if buffer && !buffer.empty?
        return buffer
      else
        return nil
      end
    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.



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
93
# File 'lib/protocol/http/body/stream.rb', line 54

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.



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

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.



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
134
# File 'lib/protocol/http/body/stream.rb', line 102

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.



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

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.



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

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