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