http-2 1.0.1 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 96271a021aa473e7c555c6ceca6509f30e4ad457051e3971304c5a841afe4ca6
4
- data.tar.gz: 5f2284ff6211f414291337d4efe260fc52bb9e18ccdb8bf19408ae781469994c
3
+ metadata.gz: b5f246529e7f64c9b6c841e7733a8bcc74ce34452fa0569c48bf1a858d47d659
4
+ data.tar.gz: ae38ca8cb1814c168fa2e90f729b12b0a3017a41d35406472dfe425308adb107
5
5
  SHA512:
6
- metadata.gz: 6a3f9b4f1a98f232405b85559c91d976c0712defe2cfe1e1ef4468bc81b7e278abd162f3e0de8828dddcdfaff11808a0cab7c794353f5224303eb8b00a4b44ec
7
- data.tar.gz: e14c1ec4652af944dfa791dacae8af519982c04b652b9ef352a1aa2377a15c80f829a2654da7c6e556680d522c7405137e4e5ab24dcb68e196eeb41e5ad54a18
6
+ metadata.gz: 5cc781007b5fa286344fb6ebc1495b4caaddc5c25749ae1ab6fb938f18f17ac50624bbf0160cb3e6661b91d1290ea350a550696e6ed28db20f6bb34c0ca0b6c7
7
+ data.tar.gz: 2bbcd46df4b28f59f7b64c68338998bae8d5199ee71942704ae32fb8e643c328b888b2579db4d8bf1b8ed220e76ea1bc27d9ef14ecc5ad21c05cb35252f2e4d1
data/lib/http/2/client.rb CHANGED
@@ -26,6 +26,7 @@ module HTTP2
26
26
 
27
27
  @local_role = :client
28
28
  @remote_role = :server
29
+ @h2c_upgrade = nil
29
30
 
30
31
  super
31
32
  end
@@ -36,9 +36,14 @@ module HTTP2
36
36
  CONNECTION_PREFACE_MAGIC = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
37
37
 
38
38
  REQUEST_MANDATORY_HEADERS = %w[:scheme :method :authority :path].freeze
39
+
39
40
  RESPONSE_MANDATORY_HEADERS = %w[:status].freeze
40
41
 
41
- EMPTY = [].freeze
42
+ CONNECTION_FRAME_TYPES = %i[settings ping goaway].freeze
43
+
44
+ HEADERS_FRAME_TYPES = %i[headers push_promise].freeze
45
+
46
+ STREAM_OPEN_STATES = %i[open half_closed_local].freeze
42
47
 
43
48
  # Connection encapsulates all of the connection, stream, flow-control,
44
49
  # error management, and other processing logic required for a well-behaved
@@ -47,13 +52,11 @@ module HTTP2
47
52
  # Note that this class should not be used directly. Instead, you want to
48
53
  # use either Client or Server class to drive the HTTP 2.0 exchange.
49
54
  #
50
- # rubocop:disable Metrics/ClassLength
51
55
  class Connection
52
56
  include FlowBuffer
53
57
  include Emitter
54
58
  include Error
55
-
56
- using StringExtensions
59
+ include BufferUtils
57
60
 
58
61
  # Connection state (:new, :closed).
59
62
  attr_reader :state
@@ -85,7 +88,6 @@ module HTTP2
85
88
  @decompressor = Header::Decompressor.new(settings)
86
89
 
87
90
  @active_stream_count = 0
88
- @last_activated_stream = 0
89
91
  @last_stream_id = 0
90
92
  @streams = {}
91
93
  @streams_recently_closed = {}
@@ -105,6 +107,10 @@ module HTTP2
105
107
  @h2c_upgrade = nil
106
108
  @closed_since = nil
107
109
  @received_frame = false
110
+
111
+ # from mixins
112
+ @listeners = Hash.new { |hash, key| hash[key] = [] }
113
+ @send_buffer = FrameBuffer.new
108
114
  end
109
115
 
110
116
  def closed?
@@ -120,10 +126,10 @@ module HTTP2
120
126
  raise ConnectionClosed if @state == :closed
121
127
  raise StreamLimitExceeded if @active_stream_count >= @remote_settings[:settings_max_concurrent_streams]
122
128
 
123
- connection_error(:protocol_error, msg: "id is smaller than previous") if @stream_id < @last_activated_stream
129
+ connection_error(:protocol_error, msg: "id is smaller than previous") if @stream_id < @last_stream_id
124
130
 
125
131
  stream = activate_stream(id: @stream_id, **args)
126
- @last_activated_stream = stream.id
132
+ @last_stream_id = stream.id
127
133
 
128
134
  @stream_id += 2
129
135
 
@@ -150,13 +156,7 @@ module HTTP2
150
156
  # @param error [Symbol]
151
157
  # @param payload [String]
152
158
  def goaway(error = :no_error, payload = nil)
153
- last_stream = if (max = @streams.max)
154
- max.first
155
- else
156
- 0
157
- end
158
-
159
- send(type: :goaway, last_stream: last_stream,
159
+ send(type: :goaway, last_stream: @last_stream_id,
160
160
  error: error, payload: payload)
161
161
  @state = :closed
162
162
  @closed_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@@ -175,7 +175,6 @@ module HTTP2
175
175
  #
176
176
  # @param settings [Array or Hash]
177
177
  def settings(payload)
178
- payload = payload.to_a
179
178
  validate_settings(@local_role, payload)
180
179
  @pending_settings << payload
181
180
  send(type: :settings, stream: 0, payload: payload)
@@ -188,7 +187,7 @@ module HTTP2
188
187
  #
189
188
  # @param data [String] Binary encoded string
190
189
  def receive(data)
191
- @recv_buffer << data
190
+ append_str(@recv_buffer, data)
192
191
 
193
192
  # Upon establishment of a TCP connection and determination that
194
193
  # HTTP/2.0 will be used by both peers, each endpoint MUST send a
@@ -202,7 +201,7 @@ module HTTP2
202
201
  raise HandshakeError unless CONNECTION_PREFACE_MAGIC.start_with? @recv_buffer
203
202
 
204
203
  return # maybe next time
205
- elsif @recv_buffer.read(24) == CONNECTION_PREFACE_MAGIC
204
+ elsif read_str(@recv_buffer, 24) == CONNECTION_PREFACE_MAGIC
206
205
  # MAGIC is OK. Send our settings
207
206
  @state = :waiting_connection_preface
208
207
  payload = @local_settings.reject { |k, v| v == SPEC_DEFAULT_CONNECTION_SETTINGS[k] }
@@ -213,14 +212,18 @@ module HTTP2
213
212
  end
214
213
 
215
214
  while (frame = @framer.parse(@recv_buffer))
215
+ # @type var stream_id: Integer
216
+ stream_id = frame[:stream]
217
+ frame_type = frame[:type]
218
+
216
219
  if is_a?(Client) && !@received_frame
217
- connection_error(:protocol_error, msg: "didn't receive settings") if frame[:type] != :settings
220
+ connection_error(:protocol_error, msg: "didn't receive settings") if frame_type != :settings
218
221
  @received_frame = true
219
222
  end
220
223
 
221
224
  # Implementations MUST discard frames
222
225
  # that have unknown or unsupported types.
223
- if frame[:type].nil?
226
+ if frame_type.nil?
224
227
  # However, extension frames that appear in
225
228
  # the middle of a header block (Section 4.3) are not permitted; these
226
229
  # MUST be treated as a connection error (Section 5.4.1) of type
@@ -234,7 +237,7 @@ module HTTP2
234
237
  # Header blocks MUST be transmitted as a contiguous sequence of frames
235
238
  # with no interleaved frames of any other type, or from any other stream.
236
239
  unless @continuation.empty?
237
- connection_error unless frame[:type] == :continuation && frame[:stream] == @continuation.first[:stream]
240
+ connection_error unless frame_type == :continuation && stream_id == @continuation.first[:stream]
238
241
 
239
242
  @continuation << frame
240
243
  unless frame[:flags].include? :end_headers
@@ -253,6 +256,8 @@ module HTTP2
253
256
  payload = @continuation.map { |f| f[:payload] }.join
254
257
 
255
258
  frame = @continuation.shift
259
+ frame_type = frame[:type]
260
+
256
261
  @continuation.clear
257
262
 
258
263
  frame.delete(:length)
@@ -266,14 +271,14 @@ module HTTP2
266
271
  # anything other than 0x0, the endpoint MUST respond with a connection
267
272
  # error (Section 5.4.1) of type PROTOCOL_ERROR.
268
273
  if connection_frame?(frame)
269
- connection_error(:protocol_error) unless frame[:stream].zero?
274
+ connection_error(:protocol_error) unless stream_id.zero?
270
275
  connection_management(frame)
271
276
  else
272
- case frame[:type]
277
+ case frame_type
273
278
  when :headers
274
279
  # When server receives even-numbered stream identifier,
275
280
  # the endpoint MUST respond with a connection error of type PROTOCOL_ERROR.
276
- connection_error if frame[:stream].even? && is_a?(Server)
281
+ connection_error if stream_id.even? && is_a?(Server)
277
282
 
278
283
  # The last frame in a sequence of HEADERS/CONTINUATION
279
284
  # frames MUST have the END_HEADERS flag set.
@@ -290,13 +295,13 @@ module HTTP2
290
295
  decode_headers(frame)
291
296
  return if @state == :closed
292
297
 
293
- stream = @streams[frame[:stream]]
298
+ stream = @streams[stream_id]
294
299
  if stream.nil?
295
300
  verify_pseudo_headers(frame)
296
301
 
297
- verify_stream_order(frame[:stream])
302
+ verify_stream_order(stream_id)
298
303
  stream = activate_stream(
299
- id: frame[:stream],
304
+ id: stream_id,
300
305
  weight: frame[:weight] || DEFAULT_WEIGHT,
301
306
  dependency: frame[:dependency] || 0,
302
307
  exclusive: frame[:exclusive] || false
@@ -327,18 +332,18 @@ module HTTP2
327
332
  # that is not currently in the "idle" state) as a connection error
328
333
  # (Section 5.4.1) of type PROTOCOL_ERROR, unless the receiver
329
334
  # recently sent a RST_STREAM frame to cancel the associated stream.
330
- parent = @streams[frame[:stream]]
335
+ parent = @streams[stream_id]
331
336
  pid = frame[:promise_stream]
332
337
 
333
338
  # if PUSH parent is recently closed, RST_STREAM the push
334
- if @streams_recently_closed[frame[:stream]]
339
+ if @streams_recently_closed[stream_id]
335
340
  send(type: :rst_stream, stream: pid, error: :refused_stream)
336
341
  return
337
342
  end
338
343
 
339
344
  connection_error(msg: "missing parent ID") if parent.nil?
340
345
 
341
- unless parent.state == :open || parent.state == :half_closed_local
346
+ unless STREAM_OPEN_STATES.include?(parent.state)
342
347
  # An endpoint might receive a PUSH_PROMISE frame after it sends
343
348
  # RST_STREAM. PUSH_PROMISE causes a stream to become "reserved".
344
349
  # The RST_STREAM does not cancel any promised stream. Therefore, if
@@ -359,21 +364,21 @@ module HTTP2
359
364
  emit(:promise, stream)
360
365
  stream << frame
361
366
  else
362
- if (stream = @streams[frame[:stream]])
367
+ if (stream = @streams[stream_id])
363
368
  stream << frame
364
- if frame[:type] == :data
369
+ if frame_type == :data
365
370
  update_local_window(frame)
366
371
  calculate_window_update(@local_window_limit)
367
372
  end
368
373
  else
369
- case frame[:type]
374
+ case frame_type
370
375
  # The PRIORITY frame can be sent for a stream in the "idle" or
371
376
  # "closed" state. This allows for the reprioritization of a
372
377
  # group of dependent streams by altering the priority of an
373
378
  # unused or closed parent stream.
374
379
  when :priority
375
380
  stream = activate_stream(
376
- id: frame[:stream],
381
+ id: stream_id,
377
382
  weight: frame[:weight] || DEFAULT_WEIGHT,
378
383
  dependency: frame[:dependency] || 0,
379
384
  exclusive: frame[:exclusive] || false
@@ -388,15 +393,17 @@ module HTTP2
388
393
  # "closed" stream. A receiver MUST NOT treat this as an error
389
394
  # (see Section 5.1).
390
395
  when :window_update
391
- stream = @streams_recently_closed[frame[:stream]]
392
- connection_error(:protocol_error, msg: "sent window update on idle stream") unless stream
396
+ unless @streams_recently_closed.key?(stream_id)
397
+ connection_error(:protocol_error, msg: "sent window update on idle stream")
398
+ end
399
+ stream = @streams_recently_closed[stream_id]
393
400
  process_window_update(frame: frame, encode: true)
394
401
  # Endpoints MUST ignore
395
402
  # WINDOW_UPDATE or RST_STREAM frames received in this state (closed), though
396
403
  # endpoints MAY choose to treat frames that arrive a significant
397
404
  # time after sending END_STREAM as a connection error.
398
405
  when :rst_stream
399
- stream = @streams_recently_closed[frame[:stream]]
406
+ stream = @streams_recently_closed[stream_id]
400
407
  connection_error(:protocol_error, msg: "sent window update on idle stream") unless stream
401
408
  else
402
409
  # An endpoint that receives an unexpected stream identifier
@@ -426,35 +433,33 @@ module HTTP2
426
433
  # @note all frames are currently delivered in FIFO order.
427
434
  # @param frame [Hash]
428
435
  def send(frame)
436
+ frame_type = frame[:type]
437
+
429
438
  emit(:frame_sent, frame)
430
- if frame[:type] == :data
439
+ if frame_type == :data
431
440
  send_data(frame, true)
432
441
 
433
- elsif frame[:type] == :rst_stream && frame[:error] == :protocol_error
442
+ elsif frame_type == :rst_stream && frame[:error] == :protocol_error
434
443
  # An endpoint can end a connection at any time. In particular, an
435
444
  # endpoint MAY choose to treat a stream error as a connection error.
436
445
 
437
- goaway(frame[:error])
446
+ goaway(:protocol_error)
438
447
  else
439
448
  # HEADERS and PUSH_PROMISE may generate CONTINUATION. Also send
440
449
  # RST_STREAM that are not protocol errors
441
- frames = encode(frame)
442
- frames.each { |f| emit(:frame, f) }
450
+ encode(frame)
443
451
  end
444
452
  end
445
453
 
446
454
  # Applies HTTP 2.0 binary encoding to the frame.
447
455
  #
448
456
  # @param frame [Hash]
449
- # @return [Array of Buffer] encoded frame
450
457
  def encode(frame)
451
- frames = if frame[:type] == :headers || frame[:type] == :push_promise
452
- encode_headers(frame) # HEADERS and PUSH_PROMISE may create more than one frame
453
- else
454
- [frame] # otherwise one frame
455
- end
456
-
457
- frames.map { |f| @framer.generate(f) }
458
+ if HEADERS_FRAME_TYPES.include?(frame[:type])
459
+ encode_headers(frame) # HEADERS and PUSH_PROMISE may create more than one frame
460
+ else
461
+ emit(:frame, @framer.generate(frame))
462
+ end
458
463
  end
459
464
 
460
465
  # Check if frame is a connection frame: SETTINGS, PING, GOAWAY, and any
@@ -463,10 +468,7 @@ module HTTP2
463
468
  # @param frame [Hash]
464
469
  # @return [Boolean]
465
470
  def connection_frame?(frame)
466
- (frame[:stream]).zero? ||
467
- frame[:type] == :settings ||
468
- frame[:type] == :ping ||
469
- frame[:type] == :goaway
471
+ frame[:stream].zero? || CONNECTION_FRAME_TYPES.include?(frame[:type])
470
472
  end
471
473
 
472
474
  # Process received connection frame (stream ID = 0).
@@ -477,14 +479,18 @@ module HTTP2
477
479
  #
478
480
  # @param frame [Hash]
479
481
  def connection_management(frame)
482
+ frame_type = frame[:type]
483
+
480
484
  case @state
481
485
  when :waiting_connection_preface
482
486
  # The first frame MUST be a SETTINGS frame at the start of a connection.
487
+ connection_error unless frame[:type] == :settings
488
+
483
489
  @state = :connected
484
490
  connection_settings(frame)
485
491
 
486
492
  when :connected
487
- case frame[:type]
493
+ case frame_type
488
494
  when :settings
489
495
  connection_settings(frame)
490
496
  when :window_update
@@ -499,23 +505,24 @@ module HTTP2
499
505
  @closed_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
500
506
  emit(:goaway, frame[:last_stream], frame[:error], frame[:payload])
501
507
  when :altsvc
508
+ origin = frame[:origin]
502
509
  # 4. The ALTSVC HTTP/2 Frame
503
510
  # An ALTSVC frame on stream 0 with empty (length 0) "Origin"
504
511
  # information is invalid and MUST be ignored.
505
- emit(frame[:type], frame) if frame[:origin] && !frame[:origin].empty?
512
+ emit(:altsvc, frame) if origin && !origin.empty?
506
513
  when :origin
507
514
  return if @h2c_upgrade || !frame[:flags].empty?
508
515
 
509
- frame[:payload].each do |origin|
510
- emit(frame[:type], origin)
516
+ frame[:payload].each do |orig|
517
+ emit(:origin, orig)
511
518
  end
512
519
  when :blocked
513
- emit(frame[:type], frame)
520
+ emit(:blocked, frame)
514
521
  else
515
522
  connection_error
516
523
  end
517
524
  when :closed
518
- case frame[:type]
525
+ case frame_type
519
526
  when :goaway
520
527
  connection_error
521
528
  when :ping
@@ -574,7 +581,7 @@ module HTTP2
574
581
  # allowed frame size (2^24-1 or 16,777,215 octets), inclusive.
575
582
  # Values outside this range MUST be treated as a connection error
576
583
  # (Section 5.4.1) of type PROTOCOL_ERROR.
577
- next if v >= 16_384 && v <= 16_777_215
584
+ next if v.between?(16_384, 16_777_215)
578
585
 
579
586
  connection_error(:protocol_error, msg: "invalid #{key} value")
580
587
  # when :settings_max_concurrent_streams
@@ -592,19 +599,19 @@ module HTTP2
592
599
  #
593
600
  # @param frame [Hash]
594
601
  def connection_settings(frame)
595
- connection_error unless frame[:type] == :settings && (frame[:stream]).zero?
596
-
597
602
  # Apply settings.
598
603
  # side =
599
604
  # local: previously sent and pended our settings should be effective
600
605
  # remote: just received peer settings should immediately be effective
601
- settings, side = if frame[:flags].include?(:ack)
602
- # Process pending settings we have sent.
603
- [@pending_settings.shift, :local]
604
- else
605
- validate_settings(@remote_role, frame[:payload])
606
- [frame[:payload], :remote]
607
- end
606
+ if frame[:flags].include?(:ack)
607
+ # Process pending settings we have sent.
608
+ settings = @pending_settings.shift
609
+ side = :local
610
+ else
611
+ validate_settings(@remote_role, frame[:payload])
612
+ settings = frame[:payload]
613
+ side = :remote
614
+ end
608
615
 
609
616
  settings.each do |key, v|
610
617
  case side
@@ -700,40 +707,48 @@ module HTTP2
700
707
 
701
708
  # Encode headers payload and update connection compressor state.
702
709
  #
703
- # @param frame [Hash]
704
- # @return [Array of Frame]
705
- def encode_headers(frame)
706
- payload = frame[:payload]
710
+ # @param headers_frame [Hash]
711
+ def encode_headers(headers_frame)
712
+ payload = headers_frame[:payload]
707
713
  begin
708
- payload = frame[:payload] = @compressor.encode(payload) unless payload.is_a?(String)
714
+ payload = headers_frame[:payload] = @compressor.encode(payload) unless payload.is_a?(String)
709
715
  rescue StandardError => e
710
716
  connection_error(:compression_error, e: e)
711
717
  end
712
718
 
713
- # if single frame, return immediately
714
- return [frame] if payload.bytesize <= @remote_settings[:settings_max_frame_size]
719
+ max_frame_size = @remote_settings[:settings_max_frame_size]
715
720
 
716
- frames = []
721
+ # if single frame, return immediately
722
+ if payload.bytesize <= max_frame_size
723
+ emit(:frame, @framer.generate(headers_frame))
724
+ return
725
+ end
717
726
 
718
- until payload.nil? || payload.empty?
719
- cont = frame.dup
727
+ # split into multiple CONTINUATION frames
728
+ headers_frame[:flags].delete(:end_headers)
729
+ headers_frame[:payload] = payload.byteslice(0, max_frame_size)
730
+ payload = payload.byteslice(max_frame_size..-1)
720
731
 
721
- # first frame remains HEADERS
722
- unless frames.empty?
723
- cont[:type] = :continuation
724
- cont[:flags] = EMPTY
725
- end
732
+ # emit first HEADERS frame
733
+ emit(:frame, @framer.generate(headers_frame))
726
734
 
727
- cont[:payload] = payload.byteslice(0, @remote_settings[:settings_max_frame_size])
728
- payload = payload.byteslice(@remote_settings[:settings_max_frame_size]..-1)
735
+ loop do
736
+ continuation_frame = headers_frame.merge(
737
+ type: :continuation,
738
+ flags: EMPTY,
739
+ payload: payload.byteslice(0, max_frame_size)
740
+ )
729
741
 
730
- frames << cont
731
- end
742
+ payload = payload.byteslice(max_frame_size..-1)
732
743
 
733
- frames.first[:flags].delete(:end_headers)
734
- frames.last[:flags] = [:end_headers]
744
+ if payload.nil? || payload.empty?
745
+ continuation_frame[:flags] = [:end_headers]
746
+ emit(:frame, @framer.generate(continuation_frame))
747
+ break
748
+ end
735
749
 
736
- frames
750
+ emit(:frame, @framer.generate(continuation_frame))
751
+ end
737
752
  end
738
753
 
739
754
  # Activates new incoming or outgoing stream and registers appropriate
@@ -759,17 +774,22 @@ module HTTP2
759
774
  # is closed, with a minimum of 15s RTT time window.
760
775
  now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
761
776
 
762
- # TODO: use a drop_while! variant whenever there is one.
763
- @streams_recently_closed = @streams_recently_closed.drop_while do |_, v|
764
- (now - v) > 15
765
- end.to_h
777
+ _, closed_since = @streams_recently_closed.first
778
+
779
+ # forego recently closed recycling if empty or the first element
780
+ # hasn't expired yet (it's ordered).
781
+ if closed_since && (now - closed_since) > 15
782
+ # discards all streams which have closed for a while.
783
+ # TODO: use a drop_while! variant whenever there is one.
784
+ @streams_recently_closed = @streams_recently_closed.drop_while do |_, since|
785
+ (now - since) > 15
786
+ end.to_h
787
+ end
766
788
 
767
789
  @streams_recently_closed[id] = Process.clock_gettime(Process::CLOCK_MONOTONIC)
768
790
  end
769
791
 
770
- stream.on(:promise, &method(:promise)) if is_a? Server
771
- stream.on(:frame, &method(:send))
772
-
792
+ stream.on(:frame, &method(:send))
773
793
  @streams[id] = stream
774
794
  end
775
795
 
@@ -821,5 +841,4 @@ module HTTP2
821
841
  yield
822
842
  end
823
843
  end
824
- # rubocop:enable Metrics/ClassLength
825
844
  end
@@ -12,7 +12,7 @@ module HTTP2
12
12
  def on(event, &block)
13
13
  raise ArgumentError, "must provide callback" unless block
14
14
 
15
- listeners(event.to_sym).push block
15
+ @listeners[event] << block
16
16
  end
17
17
 
18
18
  # Subscribe to next event (at most once) for specified type.
@@ -32,16 +32,9 @@ module HTTP2
32
32
  # @param args [Array] arguments to be passed to the callbacks
33
33
  # @param block [Proc] callback function
34
34
  def emit(event, *args, &block)
35
- listeners(event).delete_if do |cb|
35
+ @listeners[event].delete_if do |cb|
36
36
  :delete == cb.call(*args, &block) # rubocop:disable Style/YodaCondition
37
37
  end
38
38
  end
39
-
40
- private
41
-
42
- def listeners(event)
43
- @listeners ||= Hash.new { |hash, key| hash[key] = [] }
44
- @listeners[event]
45
- end
46
39
  end
47
40
  end
@@ -1,24 +1,42 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTP2
4
- module StringExtensions
5
- refine String do
6
- def read(n)
7
- return "".b if n == 0
8
-
9
- chunk = byteslice(0..n - 1)
10
- remaining = byteslice(n..-1)
11
- remaining ? replace(remaining) : clear
12
- chunk
4
+ module BufferUtils
5
+ if RUBY_VERSION > "3.4.0"
6
+ def append_str(str, data)
7
+ str.append_as_bytes(data)
13
8
  end
9
+ else
10
+ def append_str(str, data)
11
+ enc = data.encoding
12
+ reset = false
14
13
 
15
- def read_uint32
16
- read(4).unpack1("N")
14
+ if enc != Encoding::BINARY
15
+ reset = true
16
+ data = data.dup if data.frozen?
17
+ data.force_encoding(Encoding::BINARY)
18
+ end
19
+ str << data
20
+ ensure
21
+ data.force_encoding(enc) if reset
17
22
  end
23
+ end
18
24
 
19
- def shift_byte
20
- read(1).ord
21
- end
25
+ def read_str(str, n)
26
+ return "".b if n == 0
27
+
28
+ chunk = str.byteslice(0..n - 1)
29
+ remaining = str.byteslice(n..-1)
30
+ remaining ? str.replace(remaining) : str.clear
31
+ chunk
32
+ end
33
+
34
+ def read_uint32(str)
35
+ read_str(str, 4).unpack1("N")
36
+ end
37
+
38
+ def shift_byte(str)
39
+ read_str(str, 1).ord
22
40
  end
23
41
  end
24
42
 
@@ -30,7 +48,7 @@ module HTTP2
30
48
  packed_str = array_to_pack.pack(template)
31
49
  case offset
32
50
  when -1
33
- buffer << packed_str
51
+ append_str(buffer, packed_str)
34
52
  when 0
35
53
  buffer.prepend(packed_str)
36
54
  else