http-2 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,8 @@ module HTTP2
7
7
  module FlowBuffer
8
8
  include Error
9
9
 
10
+ attr_reader :send_buffer
11
+
10
12
  MAX_WINDOW_SIZE = (2 << 30) - 1
11
13
 
12
14
  # Amount of buffered data. Only DATA payloads are subject to flow stream
@@ -14,7 +16,7 @@ module HTTP2
14
16
  #
15
17
  # @return [Integer]
16
18
  def buffered_amount
17
- send_buffer.bytesize
19
+ @send_buffer.bytesize
18
20
  end
19
21
 
20
22
  def flush
@@ -23,13 +25,9 @@ module HTTP2
23
25
 
24
26
  private
25
27
 
26
- def send_buffer
27
- @send_buffer ||= FrameBuffer.new
28
- end
29
-
30
28
  def update_local_window(frame)
31
29
  frame_size = frame[:payload].bytesize
32
- frame_size += frame[:padding] || 0
30
+ frame_size += frame.fetch(:padding, 0)
33
31
  @local_window -= frame_size
34
32
  end
35
33
 
@@ -69,31 +67,47 @@ module HTTP2
69
67
  # @param frame [Hash]
70
68
  # @param encode [Boolean] set to true by connection
71
69
  def send_data(frame = nil, encode = false)
72
- send_buffer << frame unless frame.nil?
70
+ if frame
71
+ if @send_buffer.empty?
72
+ frame_size = frame[:payload].bytesize
73
+ end_stream = frame[:flags].include?(:end_stream)
74
+ # if buffer is empty, and frame is either end 0 length OR
75
+ # is within available window size, skip buffering and send immediately.
76
+ if @remote_window.positive?
77
+ return send_frame(frame, encode) if frame_size <= @remote_window
78
+ elsif frame_size.zero? && end_stream
79
+ return send_frame(frame, encode)
80
+ end
81
+ end
73
82
 
74
- while (frame = send_buffer.retrieve(@remote_window))
83
+ @send_buffer << frame
84
+ end
75
85
 
76
- # puts "#{self.class} -> #{@remote_window}"
77
- sent = frame[:payload].bytesize
86
+ while (frame = @send_buffer.retrieve(@remote_window))
87
+ send_frame(frame, encode)
88
+ end
89
+ end
78
90
 
79
- manage_state(frame) do
80
- if encode
81
- encode(frame).each { |f| emit(:frame, f) }
82
- else
83
- emit(:frame, frame)
84
- end
85
- @remote_window -= sent
91
+ def send_frame(frame, encode)
92
+ sent = frame[:payload].bytesize
93
+
94
+ manage_state(frame) do
95
+ if encode
96
+ encode(frame)
97
+ else
98
+ emit(:frame, frame)
86
99
  end
100
+ @remote_window -= sent
87
101
  end
88
102
  end
89
103
 
90
104
  def process_window_update(frame:, encode: false)
91
105
  return if frame[:ignore]
92
106
 
93
- if frame[:increment]
94
- raise ProtocolError, "increment MUST be higher than zero" if frame[:increment].zero?
107
+ if (increment = frame[:increment])
108
+ raise ProtocolError, "increment MUST be higher than zero" if increment.zero?
95
109
 
96
- @remote_window += frame[:increment]
110
+ @remote_window += increment
97
111
  error(:flow_control_error, msg: "window size too large") if @remote_window > MAX_WINDOW_SIZE
98
112
  end
99
113
  send_data(nil, encode)
@@ -114,45 +128,41 @@ module HTTP2
114
128
  end
115
129
 
116
130
  def empty?
117
- @bytesize.zero?
131
+ @buffer.empty?
118
132
  end
119
133
 
120
134
  def retrieve(window_size)
121
135
  frame = @buffer.first or return
122
136
 
123
137
  frame_size = frame[:payload].bytesize
124
- end_stream = frame[:flags].include? :end_stream
138
+ end_stream = frame[:flags].include?(:end_stream)
125
139
 
126
140
  # Frames with zero length with the END_STREAM flag set (that
127
141
  # is, an empty DATA frame) MAY be sent if there is no available space
128
142
  # in either flow control window.
129
- return if window_size <= 0 && !(frame_size == 0 && end_stream)
130
-
131
- @buffer.shift
143
+ return if window_size <= 0 && !(frame_size.zero? && end_stream)
132
144
 
133
145
  if frame_size > window_size
134
- payload = frame[:payload]
135
146
  chunk = frame.dup
147
+ payload = frame[:payload]
136
148
 
137
149
  # Split frame so that it fits in the window
138
150
  # TODO: consider padding!
139
- frame_bytes = payload.byteslice(0, window_size)
140
- payload = payload.byteslice(window_size..-1)
141
151
 
142
- frame[:payload] = frame_bytes
143
- frame[:length] = frame_bytes.bytesize
144
- chunk[:payload] = payload
145
- chunk[:length] = payload.bytesize
152
+ chunk[:payload] = payload.byteslice(0, window_size)
153
+ chunk[:length] = window_size
154
+ frame[:payload] = payload.byteslice(window_size..-1)
155
+ frame[:length] = frame_size - window_size
146
156
 
147
157
  # if no longer last frame in sequence...
148
- frame[:flags] -= [:end_stream] if end_stream
158
+ chunk[:flags] -= [:end_stream] if end_stream
149
159
 
150
- @buffer.unshift(chunk)
151
160
  @bytesize -= window_size
161
+ chunk
152
162
  else
153
163
  @bytesize -= frame_size
164
+ @buffer.shift
154
165
  end
155
- frame
156
166
  end
157
167
  end
158
168
  end
data/lib/http/2/framer.rb CHANGED
@@ -36,6 +36,8 @@ module HTTP2
36
36
  origin: 0xc
37
37
  }.freeze
38
38
 
39
+ FRAME_TYPES_BY_NAME = FRAME_TYPES.invert.freeze
40
+
39
41
  FRAME_TYPES_WITH_PADDING = %i[data headers push_promise].freeze
40
42
 
41
43
  # Per frame flags as defined by the spec
@@ -126,30 +128,43 @@ module HTTP2
126
128
  # @param buffer [String] buffer to pack bytes into
127
129
  # @return [String]
128
130
  def common_header(frame, buffer:)
129
- raise CompressionError, "Invalid frame type (#{frame[:type]})" unless FRAME_TYPES[frame[:type]]
131
+ type = frame[:type]
132
+
133
+ raise CompressionError, "Invalid frame type (#{type})" unless FRAME_TYPES[type]
134
+
135
+ length = frame[:length]
136
+
137
+ raise CompressionError, "Frame size is too large: #{length}" if length > @remote_max_frame_size
130
138
 
131
- raise CompressionError, "Frame size is too large: #{frame[:length]}" if frame[:length] > @remote_max_frame_size
139
+ raise CompressionError, "Frame size is invalid: #{length}" if length < 0
132
140
 
133
- raise CompressionError, "Frame size is invalid: #{frame[:length]}" if frame[:length] < 0
141
+ stream_id = frame.fetch(:stream, 0)
134
142
 
135
- raise CompressionError, "Stream ID (#{frame[:stream]}) is too large" if frame[:stream] > MAX_STREAM_ID
143
+ raise CompressionError, "Stream ID (#{stream_id}) is too large" if stream_id > MAX_STREAM_ID
136
144
 
137
- if frame[:type] == :window_update && frame[:increment] > MAX_WINDOWINC
145
+ if type == :window_update && frame[:increment] > MAX_WINDOWINC
138
146
  raise CompressionError, "Window increment (#{frame[:increment]}) is too large"
139
147
  end
140
148
 
149
+ header = buffer
150
+
151
+ if buffer.frozen?
152
+ header = String.new("", encoding: Encoding::BINARY, capacity: buffer.bytesize + 9) # header length
153
+ header << buffer
154
+ end
155
+
141
156
  pack([
142
- (frame[:length] >> FRAME_LENGTH_HISHIFT),
143
- (frame[:length] & FRAME_LENGTH_LOMASK),
144
- FRAME_TYPES[frame[:type]],
157
+ (length >> FRAME_LENGTH_HISHIFT),
158
+ (length & FRAME_LENGTH_LOMASK),
159
+ FRAME_TYPES[type],
145
160
  frame[:flags].reduce(0) do |acc, f|
146
- position = FRAME_FLAGS[frame[:type]][f]
147
- raise CompressionError, "Invalid frame flag (#{f}) for #{frame[:type]}" unless position
161
+ position = FRAME_FLAGS[type][f]
162
+ raise CompressionError, "Invalid frame flag (#{f}) for #{type}" unless position
148
163
 
149
164
  acc | (1 << position)
150
165
  end,
151
- frame[:stream]
152
- ], HEADERPACK, buffer: buffer, offset: 0) # 8+16,8,8,32
166
+ stream_id
167
+ ], HEADERPACK, buffer: header, offset: 0) # 8+16,8,8,32
153
168
  end
154
169
 
155
170
  # Decodes common 9-byte header.
@@ -157,19 +172,21 @@ module HTTP2
157
172
  # @param buf [Buffer]
158
173
  # @return [Hash] the corresponding frame
159
174
  def read_common_header(buf)
160
- frame = {}
161
175
  len_hi, len_lo, type, flags, stream = buf.byteslice(0, 9).unpack(HEADERPACK)
162
176
 
163
- frame[:length] = (len_hi << FRAME_LENGTH_HISHIFT) | len_lo
164
- frame[:type], = FRAME_TYPES.find { |_t, pos| type == pos }
165
- if frame[:type]
166
- frame[:flags] = FRAME_FLAGS[frame[:type]].each_with_object([]) do |(name, pos), acc|
167
- acc << name if flags.anybits?((1 << pos))
168
- end
169
- end
177
+ type = FRAME_TYPES_BY_NAME[type]
178
+ length = (len_hi << FRAME_LENGTH_HISHIFT) | len_lo
170
179
 
171
- frame[:stream] = stream & RBIT
172
- frame
180
+ return { length: length } unless type
181
+
182
+ {
183
+ type: type,
184
+ flags: FRAME_FLAGS[type].filter_map do |name, pos|
185
+ name if flags.anybits?((1 << pos))
186
+ end,
187
+ length: length,
188
+ stream: stream & RBIT
189
+ }
173
190
  end
174
191
 
175
192
  # Generates encoded HTTP/2 frame.
@@ -177,53 +194,60 @@ module HTTP2
177
194
  #
178
195
  # @param frame [Hash]
179
196
  def generate(frame)
180
- bytes = "".b
181
197
  length = 0
182
-
183
- frame[:flags] ||= []
184
- frame[:stream] ||= 0
198
+ frame[:flags] ||= EMPTY
185
199
 
186
200
  case frame[:type]
187
- when :data
188
- bytes << frame[:payload]
189
- bytes.force_encoding(Encoding::BINARY)
190
- length += frame[:payload].bytesize
201
+ when :data, :continuation
202
+ bytes = frame[:payload]
203
+ length = bytes.bytesize
191
204
 
192
205
  when :headers
206
+ headers = frame[:payload]
207
+
193
208
  if frame[:weight] || frame[:dependency] || !frame[:exclusive].nil?
194
209
  unless frame[:weight] && frame[:dependency] && !frame[:exclusive].nil?
195
210
  raise CompressionError, "Must specify all of priority parameters for #{frame[:type]}"
196
211
  end
197
212
 
198
- frame[:flags] += [:priority] unless frame[:flags].include? :priority
213
+ frame[:flags] += [:priority] unless frame[:flags].include?(:priority)
199
214
  end
200
215
 
201
- if frame[:flags].include? :priority
216
+ if frame[:flags].include?(:priority)
217
+ length = 5 + headers.bytesize
218
+ bytes = String.new("", encoding: Encoding::BINARY, capacity: length)
202
219
  pack([(frame[:exclusive] ? EBIT : 0) | (frame[:dependency] & RBIT)], UINT32, buffer: bytes)
203
220
  pack([frame[:weight] - 1], UINT8, buffer: bytes)
204
- length += 5
221
+ append_str(bytes, headers)
222
+ else
223
+ length = headers.bytesize
224
+ bytes = headers
205
225
  end
206
226
 
207
- bytes << frame[:payload]
208
- length += frame[:payload].bytesize
209
-
210
227
  when :priority
211
228
  unless frame[:weight] && frame[:dependency] && !frame[:exclusive].nil?
212
229
  raise CompressionError, "Must specify all of priority parameters for #{frame[:type]}"
213
230
  end
214
231
 
232
+ length = 5
233
+ bytes = String.new("", encoding: Encoding::BINARY, capacity: length)
215
234
  pack([(frame[:exclusive] ? EBIT : 0) | (frame[:dependency] & RBIT)], UINT32, buffer: bytes)
216
235
  pack([frame[:weight] - 1], UINT8, buffer: bytes)
217
- length += 5
218
236
 
219
237
  when :rst_stream
238
+ length = 4
239
+ bytes = String.new("", encoding: Encoding::BINARY, capacity: length)
220
240
  pack_error(frame[:error], buffer: bytes)
221
- length += 4
222
241
 
223
242
  when :settings
224
- raise CompressionError, "Invalid stream ID (#{frame[:stream]})" if (frame[:stream]).nonzero?
243
+ if (stream_id = frame[:stream]) && stream_id.nonzero?
244
+ raise CompressionError, "Invalid stream ID (#{stream_id})"
245
+ end
225
246
 
226
- frame[:payload].each do |(k, v)|
247
+ settings = frame[:payload]
248
+ bytes = String.new("", encoding: Encoding::BINARY, capacity: length)
249
+
250
+ settings.each do |(k, v)|
227
251
  if k.is_a? Integer # rubocop:disable Style/GuardClause
228
252
  DEFINED_SETTINGS.value?(k) || next
229
253
  else
@@ -238,42 +262,42 @@ module HTTP2
238
262
  end
239
263
 
240
264
  when :push_promise
265
+ length = 4 + frame[:payload].bytesize
266
+ bytes = String.new("", encoding: Encoding::BINARY, capacity: length)
241
267
  pack([frame[:promise_stream] & RBIT], UINT32, buffer: bytes)
242
- bytes << frame[:payload]
243
- length += 4 + frame[:payload].bytesize
268
+ append_str(bytes, frame[:payload])
244
269
 
245
270
  when :ping
246
- raise CompressionError, "Invalid payload size (#{frame[:payload].size} != 8 bytes)" if frame[:payload].bytesize != 8
271
+ bytes = frame[:payload]
272
+ raise CompressionError, "Invalid payload size (#{bytes.size} != 8 bytes)" if bytes.bytesize != 8
247
273
 
248
- bytes << frame[:payload]
249
- length += 8
274
+ length = 8
250
275
 
251
276
  when :goaway
277
+ data = frame[:payload]
278
+ length = 8
279
+ length += data.bytesize if data
280
+ bytes = String.new("", encoding: Encoding::BINARY, capacity: length)
281
+
252
282
  pack([frame[:last_stream] & RBIT], UINT32, buffer: bytes)
253
283
  pack_error(frame[:error], buffer: bytes)
254
- length += 8
255
284
 
256
- if frame[:payload]
257
- bytes << frame[:payload]
258
- length += frame[:payload].bytesize
259
- end
285
+ append_str(bytes, data) if data
260
286
 
261
287
  when :window_update
288
+ length = 4
289
+ bytes = String.new("", encoding: Encoding::BINARY, capacity: length)
262
290
  pack([frame[:increment] & RBIT], UINT32, buffer: bytes)
263
- length += 4
264
-
265
- when :continuation
266
- bytes << frame[:payload]
267
- length += frame[:payload].bytesize
268
291
 
269
292
  when :altsvc
293
+ length = 6
294
+ bytes = String.new("", encoding: Encoding::BINARY, capacity: length)
270
295
  pack([frame[:max_age], frame[:port]], UINT32 + UINT16, buffer: bytes)
271
- length += 6
272
296
  if frame[:proto]
273
297
  raise CompressionError, "Proto too long" if frame[:proto].bytesize > 255
274
298
 
275
299
  pack([frame[:proto].bytesize], UINT8, buffer: bytes)
276
- bytes << frame[:proto]
300
+ append_str(bytes, frame[:proto])
277
301
  length += 1 + frame[:proto].bytesize
278
302
  else
279
303
  pack([0], UINT8, buffer: bytes)
@@ -283,24 +307,25 @@ module HTTP2
283
307
  raise CompressionError, "Host too long" if frame[:host].bytesize > 255
284
308
 
285
309
  pack([frame[:host].bytesize], UINT8, buffer: bytes)
286
- bytes << frame[:host]
310
+ append_str(bytes, frame[:host])
287
311
  length += 1 + frame[:host].bytesize
288
312
  else
289
313
  pack([0], UINT8, buffer: bytes)
290
314
  length += 1
291
315
  end
292
316
  if frame[:origin]
293
- bytes << frame[:origin]
317
+ append_str(bytes, frame[:origin])
294
318
  length += frame[:origin].bytesize
295
319
  end
296
320
 
297
321
  when :origin
298
- frame[:payload].each do |origin|
322
+ origins = frame[:payload]
323
+ length = origins.sum(&:bytesize) + (2 * origins.size)
324
+ bytes = String.new("", encoding: Encoding::BINARY, capacity: length)
325
+ origins.each do |origin|
299
326
  pack([origin.bytesize], UINT16, buffer: bytes)
300
- bytes << origin
301
- length += 2 + origin.bytesize
327
+ append_str(bytes, origin)
302
328
  end
303
-
304
329
  end
305
330
 
306
331
  # Process padding.
@@ -319,12 +344,12 @@ module HTTP2
319
344
 
320
345
  length += padlen
321
346
  pack([padlen -= 1], UINT8, buffer: bytes, offset: 0)
322
- frame[:flags] << :padded
347
+ frame[:flags] += [:padded]
323
348
 
324
349
  # Padding: Padding octets that contain no application semantic value.
325
350
  # Padding octets MUST be set to zero when sending and ignored when
326
351
  # receiving.
327
- bytes << ("\0" * padlen)
352
+ append_str(bytes, ("\0" * padlen))
328
353
  end
329
354
 
330
355
  frame[:length] = length
@@ -339,22 +364,27 @@ module HTTP2
339
364
  return if buf.size < 9
340
365
 
341
366
  frame = read_common_header(buf)
342
- return if buf.size < 9 + frame[:length]
343
367
 
344
- raise ProtocolError, "payload too large" if frame[:length] > @local_max_frame_size
368
+ type = frame[:type]
369
+ length = frame[:length]
370
+ flags = frame[:flags]
371
+
372
+ return if buf.size < 9 + length
373
+
374
+ raise ProtocolError, "payload too large" if length > @local_max_frame_size
345
375
 
346
376
  read_str(buf, 9)
347
- payload = read_str(buf, frame[:length])
377
+ payload = read_str(buf, length)
348
378
 
349
379
  # Implementations MUST discard frames
350
380
  # that have unknown or unsupported types.
351
381
  # - http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-5.5
352
- return frame if frame[:type].nil?
382
+ return frame unless type
353
383
 
354
384
  # Process padding
355
385
  padlen = 0
356
- if FRAME_TYPES_WITH_PADDING.include?(frame[:type])
357
- padded = frame[:flags].include?(:padded)
386
+ if FRAME_TYPES_WITH_PADDING.include?(type)
387
+ padded = flags.include?(:padded)
358
388
  if padded
359
389
  padlen = read_str(payload, 1).unpack1(UINT8)
360
390
  frame[:padding] = padlen + 1
@@ -362,15 +392,15 @@ module HTTP2
362
392
 
363
393
  payload = payload.byteslice(0, payload.bytesize - padlen) if padlen > 0
364
394
  frame[:length] -= frame[:padding]
365
- frame[:flags].delete(:padded)
395
+ flags.delete(:padded)
366
396
  end
367
397
  end
368
398
 
369
- case frame[:type]
399
+ case type
370
400
  when :data, :ping, :continuation
371
- frame[:payload] = read_str(payload, frame[:length])
401
+ frame[:payload] = read_str(payload, length)
372
402
  when :headers
373
- if frame[:flags].include? :priority
403
+ if flags.include?(:priority)
374
404
  e_sd = read_uint32(payload)
375
405
  frame[:dependency] = e_sd & RBIT
376
406
  frame[:exclusive] = e_sd.anybits?(EBIT)
@@ -378,9 +408,9 @@ module HTTP2
378
408
  frame[:weight] = weight
379
409
  payload = payload.byteslice(1..-1)
380
410
  end
381
- frame[:payload] = read_str(payload, frame[:length])
411
+ frame[:payload] = read_str(payload, length)
382
412
  when :priority
383
- raise FrameSizeError, "Invalid length for PRIORITY_STREAM (#{frame[:length]} != 5)" if frame[:length] != 5
413
+ raise FrameSizeError, "Invalid length for PRIORITY_STREAM (#{length} != 5)" if length != 5
384
414
 
385
415
  e_sd = read_uint32(payload)
386
416
  frame[:dependency] = e_sd & RBIT
@@ -389,7 +419,7 @@ module HTTP2
389
419
  frame[:weight] = weight
390
420
  payload = payload.byteslice(1..-1)
391
421
  when :rst_stream
392
- raise FrameSizeError, "Invalid length for RST_STREAM (#{frame[:length]} != 4)" if frame[:length] != 4
422
+ raise FrameSizeError, "Invalid length for RST_STREAM (#{length} != 4)" if length != 4
393
423
 
394
424
  frame[:error] = unpack_error read_uint32(payload)
395
425
 
@@ -397,9 +427,9 @@ module HTTP2
397
427
  # NOTE: frame[:length] might not match the number of frame[:payload]
398
428
  # because unknown extensions are ignored.
399
429
  frame[:payload] = []
400
- raise ProtocolError, "Invalid settings payload length" unless (frame[:length] % 6).zero?
430
+ raise ProtocolError, "Invalid settings payload length" unless (length % 6).zero?
401
431
 
402
- raise ProtocolError, "Invalid stream ID (#{frame[:stream]})" if (frame[:stream]).nonzero?
432
+ raise ProtocolError, "Invalid stream ID (#{frame[:stream]})" if frame[:stream].nonzero?
403
433
 
404
434
  (frame[:length] / 6).times do
405
435
  id = read_str(payload, 2).unpack1(UINT16)
@@ -412,17 +442,15 @@ module HTTP2
412
442
  end
413
443
  when :push_promise
414
444
  frame[:promise_stream] = read_uint32(payload) & RBIT
415
- frame[:payload] = read_str(payload, frame[:length])
445
+ frame[:payload] = read_str(payload, length)
416
446
  when :goaway
417
447
  frame[:last_stream] = read_uint32(payload) & RBIT
418
448
  frame[:error] = unpack_error read_uint32(payload)
419
449
 
420
- size = frame[:length] - 8 # for last_stream and error
450
+ size = length - 8 # for last_stream and error
421
451
  frame[:payload] = read_str(payload, size) if size > 0
422
452
  when :window_update
423
- if frame[:length] % 4 != 0
424
- raise FrameSizeError, "Invalid length for WINDOW_UPDATE (#{frame[:length]} not multiple of 4)"
425
- end
453
+ raise FrameSizeError, "Invalid length for WINDOW_UPDATE (#{length} not multiple of 4)" if length % 4 != 0
426
454
 
427
455
  frame[:increment] = read_uint32(payload) & RBIT
428
456
  when :altsvc
@@ -5,6 +5,7 @@ module HTTP2
5
5
  # Responsible for encoding header key-value pairs using HPACK algorithm.
6
6
  class Compressor
7
7
  include PackingExtensions
8
+ include BufferUtils
8
9
 
9
10
  # @param options [Hash] encoding options
10
11
  def initialize(options = {})
@@ -34,8 +35,8 @@ module HTTP2
34
35
  # @param buffer [String] buffer to pack bytes into
35
36
  # @param offset [Integer] offset to insert packed bytes in buffer
36
37
  # @return [String] binary string
37
- def integer(i, n, buffer:, offset: 0)
38
- limit = (2**n) - 1
38
+ def integer(i, n, buffer:, offset: buffer.size)
39
+ limit = (1 << n) - 1
39
40
  return pack([i], "C", buffer: buffer, offset: offset) if i < limit
40
41
 
41
42
  bytes = []
@@ -70,19 +71,23 @@ module HTTP2
70
71
  # :shorter Use Huffman when the result is strictly shorter
71
72
  #
72
73
  # @param str [String]
74
+ # @param buffer [String]
73
75
  # @return [String] binary string
74
- def string(str)
76
+ def string(str, buffer = "".b)
75
77
  case @cc.options[:huffman]
76
78
  when :always
77
- huffman_string(str)
79
+ huffman_string(str, buffer)
78
80
  when :never
79
- plain_string(str)
81
+ plain_string(str, buffer)
80
82
  else
81
- huffman = huffman_string(str)
82
-
83
- plain = plain_string(str)
84
-
85
- huffman.bytesize < plain.bytesize ? huffman : plain
83
+ huffman = Huffman.encode(str)
84
+ if huffman.bytesize < str.bytesize
85
+ huffman_offset = buffer.bytesize
86
+ append_str(buffer, huffman)
87
+ set_huffman_size(buffer, huffman_offset)
88
+ else
89
+ plain_string(str, buffer)
90
+ end
86
91
  end
87
92
  end
88
93
 
@@ -92,27 +97,30 @@ module HTTP2
92
97
  # @param buffer [String]
93
98
  # @return [Buffer]
94
99
  def header(h, buffer = "".b)
95
- rep = HEADREP[h[:type]]
100
+ type = h[:type]
101
+ rep = HEADREP[type]
102
+ offset = buffer.size
96
103
 
97
- case h[:type]
104
+ case type
98
105
  when :indexed
99
106
  integer(h[:name] + 1, rep[:prefix], buffer: buffer)
100
107
  when :changetablesize
101
108
  integer(h[:value], rep[:prefix], buffer: buffer)
102
109
  else
103
- if h[:name].is_a? Integer
104
- integer(h[:name] + 1, rep[:prefix], buffer: buffer)
110
+ name = h[:name]
111
+ if name.is_a? Integer
112
+ integer(name + 1, rep[:prefix], buffer: buffer)
105
113
  else
106
114
  integer(0, rep[:prefix], buffer: buffer)
107
- buffer << string(h[:name])
115
+ string(name, buffer)
108
116
  end
109
117
 
110
- buffer << string(h[:value])
118
+ string(h[:value], buffer)
111
119
  end
112
120
 
113
121
  # set header representation pattern on first byte
114
- fb = buffer.ord | rep[:pattern]
115
- buffer.setbyte(0, fb)
122
+ fb = buffer[offset].ord | rep[:pattern]
123
+ buffer.setbyte(offset, fb)
116
124
 
117
125
  buffer
118
126
  end
@@ -123,11 +131,10 @@ module HTTP2
123
131
  # @return [Buffer]
124
132
  def encode(headers)
125
133
  buffer = "".b
126
- pseudo_headers, regular_headers = headers.partition { |f, _| f.start_with? ":" }
127
- headers = [*pseudo_headers, *regular_headers]
128
- commands = @cc.encode(headers)
129
- commands.each do |cmd|
130
- buffer << header(cmd)
134
+ headers.partition { |f, _| f.start_with? ":" }.each do |hs|
135
+ @cc.encode(hs) do |cmd|
136
+ header(cmd, buffer)
137
+ end
131
138
  end
132
139
 
133
140
  buffer
@@ -136,22 +143,31 @@ module HTTP2
136
143
  private
137
144
 
138
145
  # @param str [String]
146
+ # @param buffer [String]
139
147
  # @return [String] binary string
140
- def huffman_string(str)
141
- huffman = Huffman.new.encode(str)
142
- integer(huffman.bytesize, 7, buffer: huffman, offset: 0)
143
- huffman.setbyte(0, huffman.ord | 0x80)
144
- huffman
148
+ def huffman_string(str, buffer = "".b)
149
+ huffman_offset = buffer.bytesize
150
+ Huffman.encode(str, buffer)
151
+ set_huffman_size(buffer, huffman_offset)
145
152
  end
146
153
 
147
154
  # @param str [String]
155
+ # @param buffer [String]
148
156
  # @return [String] binary string
149
- def plain_string(str)
150
- plain = "".b
157
+ def plain_string(str, plain = "".b)
151
158
  integer(str.bytesize, 7, buffer: plain)
152
- plain << str.dup.force_encoding(Encoding::BINARY)
159
+ append_str(plain, str)
153
160
  plain
154
161
  end
162
+
163
+ # @param buffer [String]
164
+ # @param huffman_offset [Integer] buffer offset where huffman string was introduced
165
+ # @return [String] binary string
166
+ def set_huffman_size(buffer, huffman_offset)
167
+ integer(buffer.bytesize - huffman_offset, 7, buffer: buffer, offset: huffman_offset)
168
+ buffer.setbyte(huffman_offset, buffer[huffman_offset].ord | 0x80)
169
+ buffer
170
+ end
155
171
  end
156
172
  end
157
173
  end