biryani 0.0.4 → 0.0.6

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/Rakefile +7 -0
  4. data/conformance/server_spec.rb +31 -0
  5. data/conformance/spec_helper.rb +14 -0
  6. data/example/echo.rb +0 -1
  7. data/example/raise_error.rb +19 -0
  8. data/lib/biryani/connection.rb +23 -18
  9. data/lib/biryani/data_buffer.rb +1 -1
  10. data/lib/biryani/error.rb +8 -0
  11. data/lib/biryani/frame/continuation.rb +4 -7
  12. data/lib/biryani/frame/data.rb +9 -12
  13. data/lib/biryani/frame/goaway.rb +6 -6
  14. data/lib/biryani/frame/headers.rb +25 -18
  15. data/lib/biryani/frame/ping.rb +5 -7
  16. data/lib/biryani/frame/priority.rb +6 -5
  17. data/lib/biryani/frame/push_promise.rb +14 -15
  18. data/lib/biryani/frame/rst_stream.rb +5 -5
  19. data/lib/biryani/frame/settings.rb +5 -5
  20. data/lib/biryani/frame/unknown.rb +1 -15
  21. data/lib/biryani/frame/window_update.rb +5 -5
  22. data/lib/biryani/frame.rb +10 -4
  23. data/lib/biryani/hpack/field.rb +42 -42
  24. data/lib/biryani/hpack/fields.rb +2 -1
  25. data/lib/biryani/hpack/huffman.rb +290 -21
  26. data/lib/biryani/hpack/integer.rb +10 -6
  27. data/lib/biryani/hpack/string.rb +6 -6
  28. data/lib/biryani/http_request.rb +3 -2
  29. data/lib/biryani/http_response.rb +23 -2
  30. data/lib/biryani/server.rb +1 -1
  31. data/lib/biryani/state.rb +14 -7
  32. data/lib/biryani/stream.rb +10 -3
  33. data/lib/biryani/streams_context.rb +12 -6
  34. data/lib/biryani/version.rb +1 -1
  35. data/lib/biryani.rb +1 -1
  36. data/spec/connection/handle_data_spec.rb +2 -2
  37. data/spec/connection/handle_headers_spec.rb +1 -1
  38. data/spec/connection/{transition_state_send_spec.rb → transition_stream_state_send_spec.rb} +3 -3
  39. data/spec/frame/continuation_spec.rb +2 -2
  40. data/spec/frame/data_spec.rb +1 -1
  41. data/spec/frame/goaway_spec.rb +1 -1
  42. data/spec/frame/headers_spec.rb +2 -2
  43. data/spec/frame/ping_spec.rb +1 -1
  44. data/spec/frame/priority_spec.rb +1 -1
  45. data/spec/frame/push_promise_spec.rb +1 -1
  46. data/spec/frame/rst_stream_spec.rb +1 -1
  47. data/spec/frame/settings_spec.rb +1 -1
  48. data/spec/frame/window_update_spec.rb +1 -1
  49. data/spec/hpack/field_spec.rb +10 -9
  50. data/spec/hpack/huffman_spec.rb +4 -4
  51. data/spec/hpack/integer_spec.rb +5 -5
  52. data/spec/hpack/string_spec.rb +2 -2
  53. data/spec/http_response_spec.rb +12 -0
  54. metadata +9 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ccf97951b31acac8912b7286f6377dedae6e404aab32e8c0eb619ef223d7349
4
- data.tar.gz: 94563414ccb1a066ca85a9dc553541ea3772bc398e3d80a970e9f26b68d7f31b
3
+ metadata.gz: 2a6614c6a74a069a08281c0f3846da8512f379e347ce77e3812c7cd22f2506bc
4
+ data.tar.gz: 5a0f61a32f576c0a693abeb2773490ddcbbdcc573b82dac16a6d3a3e82bf2e0e
5
5
  SHA512:
6
- metadata.gz: c5e5cf9d6fc22cfe738483638b414bf0a82575030d9b4c0e400d5beab27da13b5b826390b563b972eca7fca8f5cbc44e8f693bf44abeab6eb360d9b09a91a3bd
7
- data.tar.gz: f961a868e92ba46f300784f12568b0cd47cebc1cda484008b870480c22f63d5de66bf3d76fcddcbc96e192179c2d493a0fb1fbfc0b6cc189ba2852ee99af009a
6
+ metadata.gz: 0c0add40f15c4e1fa9110a295f9fda392aae21a6ad4dd062a0d3a31fd5dd208f8a384fbcf9009a6fc29e6a940f4fc755f9da0f3e244f68292ee41323cd10e82f
7
+ data.tar.gz: 1af73e5c917c095dfcb635fd7c09548b4330114125e00df539356fb270d8f083e76c0d25262652f6781e4b8415fc3c2da91083b0af19b20ac7175a70b5c28bc1
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 4.0.0
1
+ 4.0.1
data/Rakefile CHANGED
@@ -5,4 +5,11 @@ require 'rubocop/rake_task'
5
5
  RuboCop::RakeTask.new
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
+ desc 'conformance test using h2spec'
9
+ RSpec::Core::RakeTask.new(:conformance) do |t|
10
+ t.pattern = 'conformance/server_spec.rb'
11
+ t.rspec_opts = %w[--out /dev/null]
12
+ t.verbose = false
13
+ end
14
+
8
15
  task default: %i[rubocop spec]
@@ -0,0 +1,31 @@
1
+ require_relative 'spec_helper'
2
+
3
+ RSpec.describe Server do
4
+ before do
5
+ @tcpserver = TCPServer.open(PORT)
6
+
7
+ Ractor.new(@tcpserver) do |socket|
8
+ server = Server.new(
9
+ Ractor.shareable_proc do |_req, res|
10
+ res.status = 200
11
+ res.content = 'OK'
12
+ end
13
+ )
14
+
15
+ server.run(socket)
16
+ end
17
+ end
18
+
19
+ let(:client) do
20
+ which('h2spec')
21
+ "h2spec --port #{PORT} --verbose"
22
+ end
23
+
24
+ after do
25
+ @tcpserver.close
26
+ end
27
+
28
+ it 'should run' do
29
+ system(client)
30
+ end
31
+ end
@@ -0,0 +1,14 @@
1
+ require 'open3'
2
+ require 'socket'
3
+ require 'biryani'
4
+
5
+ # rubocop: disable Style/MixinUsage
6
+ include Biryani
7
+ # rubocop: enable Style/MixinUsage
8
+
9
+ PORT = 8888
10
+
11
+ def which(cmd)
12
+ o, = Open3.capture3("which #{cmd}")
13
+ warn "conformance task require `#{cmd}`. Install `#{cmd}`." if o.empty?
14
+ end
data/example/echo.rb CHANGED
@@ -13,7 +13,6 @@ server = Biryani::Server.new(
13
13
  # @param res [Biryani::HTTPResponse]
14
14
  Ractor.shareable_proc do |req, res|
15
15
  res.status = 200
16
-
17
16
  res.content = if req.method.upcase == 'POST'
18
17
  req.content
19
18
  else
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH << "#{__dir__}/../lib"
4
+
5
+ require 'socket'
6
+ require 'biryani'
7
+
8
+ port = ARGV[0] || 8888
9
+ socket = TCPServer.new(port)
10
+
11
+ server = Biryani::Server.new(
12
+ Ractor.shareable_proc do
13
+ raise 'error'
14
+ end
15
+ )
16
+ server.run(socket)
17
+
18
+ # $ bundle exec ruby example/raise_error.rb
19
+ # $ curl -v --http2-prior-knowledge http://localhost:8888
@@ -44,10 +44,11 @@ module Biryani
44
44
 
45
45
  recv_loop(io.clone)
46
46
  send_loop(io)
47
- rescue StandardError
47
+ rescue StandardError => e
48
+ puts e.backtrace
48
49
  self.class.do_send(io, Frame::Goaway.new(0, @streams_ctx.last_stream_id, ErrorCode::INTERNAL_ERROR, 'internal error'), true)
49
50
  ensure
50
- io&.close_write
51
+ io.close_write
51
52
  end
52
53
 
53
54
  # @param io [IO]
@@ -57,7 +58,7 @@ module Biryani
57
58
  obj = Frame.read(io_)
58
59
  break if obj.nil?
59
60
 
60
- sock_ << obj
61
+ sock_.send(obj, move: true)
61
62
  end
62
63
  end
63
64
  end
@@ -71,15 +72,14 @@ module Biryani
71
72
  def send_loop(io)
72
73
  loop do
73
74
  ports = @streams_ctx.txs
74
- ports << @sock
75
- next if ports.empty?
75
+ break if ports.empty? && @sock.closed?
76
76
 
77
- port, obj = Ractor.select(*ports)
77
+ port, obj = Ractor.select(@sock, *ports)
78
78
  if port == @sock
79
79
  if Biryani.err?(obj)
80
80
  reply_frame = Biryani.unwrap(obj, @streams_ctx.last_stream_id)
81
81
  self.class.do_send(io, reply_frame, true)
82
- close if self.class.transition_state_send(reply_frame, @streams_ctx)
82
+ close if self.class.transition_stream_state_send(reply_frame, @streams_ctx)
83
83
  elsif obj.length > @settings[SettingsID::SETTINGS_MAX_FRAME_SIZE]
84
84
  self.class.do_send(io, Frame::Goaway.new(0, @streams_ctx.last_stream_id, ErrorCode::FRAME_SIZE_ERROR, 'payload length greater than SETTINGS_MAX_FRAME_SIZE'), true)
85
85
  close
@@ -93,7 +93,7 @@ module Biryani
93
93
  @streams_ctx[reply_frame.stream_id].recv_window.increase!(reply_frame.window_size_increment)
94
94
  end
95
95
 
96
- close if self.class.transition_state_send(reply_frame, @streams_ctx)
96
+ close if self.class.transition_stream_state_send(reply_frame, @streams_ctx)
97
97
  end
98
98
  end
99
99
  else
@@ -129,8 +129,12 @@ module Biryani
129
129
  #
130
130
  # @return [Array<Object>, Array<ConnectionError>, Array<StreamError>] frames or errors
131
131
  # rubocop: disable Metrics/CyclomaticComplexity
132
+ # rubocop: disable Metrics/PerceivedComplexity
132
133
  def handle_connection_frame(frame)
133
134
  typ = frame.f_type
135
+ return [ConnectionError.new(ErrorCode::PROTOCOL_ERROR, "invalid frame type #{format('0x%02x', typ)} for stream identifier #{format('0x%02x', stream_id)}")] \
136
+ if @streams_ctx.receiving_continuation? && ([FrameType::SETTINGS, FrameType::PING, FrameType::WINDOW_UPDATE].include?(typ) || FrameType.unknown?(typ))
137
+
134
138
  case typ
135
139
  when FrameType::DATA, FrameType::HEADERS, FrameType::PRIORITY, FrameType::RST_STREAM, FrameType::PUSH_PROMISE, FrameType::CONTINUATION
136
140
  [ConnectionError.new(ErrorCode::PROTOCOL_ERROR, "invalid frame type #{format('0x%02x', typ)} for stream identifier 0x00")]
@@ -163,6 +167,7 @@ module Biryani
163
167
  end
164
168
  end
165
169
  # rubocop: enable Metrics/CyclomaticComplexity
170
+ # rubocop: enable Metrics/PerceivedComplexity
166
171
 
167
172
  # @param frame [Object]
168
173
  #
@@ -179,7 +184,7 @@ module Biryani
179
184
  max_streams = @peer_settings[SettingsID::SETTINGS_MAX_CONCURRENT_STREAMS]
180
185
  send_initial_window_size = @peer_settings[SettingsID::SETTINGS_INITIAL_WINDOW_SIZE]
181
186
  recv_initial_window_size = @settings[SettingsID::SETTINGS_INITIAL_WINDOW_SIZE]
182
- obj = self.class.transition_state_recv(frame, @streams_ctx, stream_id, max_streams, send_initial_window_size, recv_initial_window_size)
187
+ obj = self.class.transition_stream_state_recv(frame, @streams_ctx, stream_id, max_streams, send_initial_window_size, recv_initial_window_size)
183
188
  return [obj] if Biryani.err?(obj)
184
189
 
185
190
  ctx = obj
@@ -241,7 +246,7 @@ module Biryani
241
246
  # @return [StreamContext, StreamError, ConnectionError]
242
247
  # rubocop: disable Metrics/CyclomaticComplexity
243
248
  # rubocop: disable Metrics/PerceivedComplexity
244
- def self.transition_state_recv(recv_frame, streams_ctx, stream_id, max_streams, send_initial_window_size, recv_initial_window_size)
249
+ def self.transition_stream_state_recv(recv_frame, streams_ctx, stream_id, max_streams, send_initial_window_size, recv_initial_window_size)
245
250
  ctx = streams_ctx[stream_id]
246
251
  return StreamError.new(ErrorCode::PROTOCOL_ERROR, stream_id, 'exceed max concurrent streams') if ctx.nil? && streams_ctx.count_active + 1 > max_streams
247
252
  return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'even-numbered stream identifier') if ctx.nil? && stream_id.even?
@@ -260,7 +265,7 @@ module Biryani
260
265
  # @param streams_ctx [StreamsContext]
261
266
  #
262
267
  # @return [Boolean] should close connection?
263
- def self.transition_state_send(send_frame, streams_ctx)
268
+ def self.transition_stream_state_send(send_frame, streams_ctx)
264
269
  stream_id = send_frame.stream_id
265
270
  typ = send_frame.f_type
266
271
  case typ
@@ -297,7 +302,7 @@ module Biryani
297
302
  do_send(io, frame, false)
298
303
  send_window.consume!(frame.length)
299
304
  streams_ctx[stream_id].send_window.consume!(frame.length)
300
- transition_state_send(frame, streams_ctx)
305
+ transition_stream_state_send(frame, streams_ctx)
301
306
  end
302
307
 
303
308
  data_buffer.store(stream_id, remains) unless remains.empty?
@@ -322,7 +327,7 @@ module Biryani
322
327
 
323
328
  frames.each do |frame|
324
329
  do_send(io, frame, false)
325
- transition_state_send(frame, streams_ctx)
330
+ transition_stream_state_send(frame, streams_ctx)
326
331
  end
327
332
  end
328
333
 
@@ -349,10 +354,10 @@ module Biryani
349
354
 
350
355
  ctx.content << data
351
356
  if ctx.half_closed_remote?
352
- obj = http_request(ctx.fragment.string, ctx.content.string, decoder)
357
+ obj = http_request(ctx.fragment, ctx.content, decoder)
353
358
  return obj if Biryani.err?(obj)
354
359
 
355
- ctx.stream.rx << obj
360
+ ctx.stream.rx.send(obj, move: true)
356
361
  end
357
362
 
358
363
  window_updates = []
@@ -370,10 +375,10 @@ module Biryani
370
375
  def self.handle_headers(headers, ctx, decoder)
371
376
  ctx.fragment << headers.fragment
372
377
  if ctx.half_closed_remote?
373
- obj = http_request(ctx.fragment.string, ctx.content.string, decoder)
374
- return [obj] if Biryani.err?(obj)
378
+ obj = http_request(ctx.fragment, ctx.content, decoder)
379
+ return obj if Biryani.err?(obj)
375
380
 
376
- ctx.stream.rx << obj
381
+ ctx.stream.rx.send(obj, move: true)
377
382
  end
378
383
 
379
384
  nil
@@ -8,7 +8,7 @@ module Biryani
8
8
  # @param data [String]
9
9
  def store(stream_id, data)
10
10
  @buffer[stream_id] = '' unless @buffer.key?(stream_id)
11
- @buffer[stream_id] += data
11
+ @buffer[stream_id] << data
12
12
  end
13
13
 
14
14
  # @param send_window [Window]
@@ -0,0 +1,8 @@
1
+ module Biryani
2
+ module Error
3
+ # Generic error, common for all classes under Biryani::Error module.
4
+ class Error < StandardError; end
5
+
6
+ class InvalidHTTPResponseError < Error; end
7
+ end
8
+ end
@@ -32,16 +32,13 @@ module Biryani
32
32
  end
33
33
 
34
34
  # @param s [String]
35
+ # @param flags [Integer]
36
+ # @param stream_id [Integer]
35
37
  #
36
38
  # @return [Continuation]
37
- def self.read(s)
38
- payload_length, _, flags, stream_id = Frame.read_header(s)
39
- return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if s[9..].bytesize != payload_length
40
-
39
+ def self.read(s, flags, stream_id)
41
40
  end_headers = Frame.read_end_headers(flags)
42
- fragment = s[9..]
43
-
44
- Continuation.new(end_headers, stream_id, fragment)
41
+ Continuation.new(end_headers, stream_id, s)
45
42
  end
46
43
  end
47
44
  end
@@ -41,26 +41,23 @@ module Biryani
41
41
  end
42
42
 
43
43
  # @param s [String]
44
+ # @param flags [Integer]
45
+ # @param stream_id [Integer]
44
46
  #
45
47
  # @return [Data]
46
- def self.read(s)
47
- payload_length, _, flags, stream_id = Frame.read_header(s)
48
- return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if s[9..].bytesize != payload_length
49
-
48
+ def self.read(s, flags, stream_id)
50
49
  padded = Frame.read_padded(flags)
51
50
  end_stream = Frame.read_end_stream(flags)
52
51
 
53
52
  if padded
54
- pad_length = s[9].unpack1('C')
55
- data_length = payload_length - pad_length - 1
56
- data = s[10...10 + data_length]
57
- padding = s[10 + data_length..]
58
- return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if pad_length >= payload_length
53
+ io = IO::Buffer.for(s)
54
+ pad_length = io.get_value(:U8, 0)
55
+ data_length = s.bytesize - pad_length - 1
56
+ data = io.get_string(1, data_length)
57
+ padding = io.get_string(1 + data_length)
59
58
  return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if padding.bytesize != pad_length
60
59
  else
61
- data = s[9..]
62
- padding = nil
63
- return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if data.bytesize != payload_length
60
+ data = s
64
61
  end
65
62
 
66
63
  Data.new(end_stream, stream_id, data, padding)
@@ -28,14 +28,14 @@ module Biryani
28
28
  end
29
29
 
30
30
  # @param s [String]
31
+ # @param _flags [Integer]
32
+ # @param stream_id [Integer]
31
33
  #
32
34
  # @return [Goaway]
33
- def self.read(s)
34
- payload_length, _, _, stream_id = Frame.read_header(s)
35
- return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if s[9..].bytesize != payload_length
36
-
37
- last_stream_id, error_code = s[9..16].unpack('NN')
38
- debug = s[17..]
35
+ def self.read(s, _flags, stream_id)
36
+ io = IO::Buffer.for(s)
37
+ last_stream_id, error_code = io.get_values(%i[U32 U32], 0)
38
+ debug = io.get_string(8)
39
39
 
40
40
  Goaway.new(stream_id, last_stream_id, error_code, debug)
41
41
  end
@@ -58,50 +58,57 @@ module Biryani
58
58
  end
59
59
 
60
60
  # @param s [String]
61
+ # @param flags [Integer]
62
+ # @param stream_id [Integer]
61
63
  #
62
64
  # @return [Headers]
63
65
  # rubocop: disable Metrics/AbcSize
64
66
  # rubocop: disable Metrics/CyclomaticComplexity
67
+ # rubocop: disable Metrics/MethodLength
65
68
  # rubocop: disable Metrics/PerceivedComplexity
66
- def self.read(s)
67
- payload_length, _, flags, stream_id = Frame.read_header(s)
68
- return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if s[9..].bytesize != payload_length
69
-
69
+ def self.read(s, flags, stream_id)
70
70
  priority = Frame.read_priority(flags)
71
71
  padded = Frame.read_padded(flags)
72
72
  end_headers = Frame.read_end_headers(flags)
73
73
  end_stream = Frame.read_end_stream(flags)
74
74
 
75
75
  if priority && padded
76
- pad_length, stream_dependency, weight = s[9..14].unpack('CNC')
77
- fragment_length = payload_length - pad_length - 6
76
+ io = IO::Buffer.for(s)
77
+ pad_length, stream_dependency, weight = io.get_values(%i[U8 U32 U8], 0)
78
+ fragment_length = s.bytesize - pad_length - 6
78
79
  # exclusive = (stream_dependency / 2**31).positive?
79
80
  stream_dependency %= 2**31
80
- fragment = s[15...15 + fragment_length]
81
- padding = s[15 + fragment_length..]
81
+ return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'cannot depend on itself') if stream_dependency == stream_id
82
+
83
+ fragment = io.get_string(6, fragment_length)
84
+ padding = io.get_string(6 + fragment_length)
82
85
  return ConnectionError.new(ErrorCode::FRAME_SIZE_ERROR, 'invalid frame') if padding.bytesize != pad_length
83
86
  elsif priority
84
- stream_dependency, weight = s[9..13].unpack('NC')
87
+ io = IO::Buffer.for(s)
88
+ stream_dependency, weight = io.get_values(%i[U32 U8], 0)
85
89
  # exclusive = (stream_dependency / 2**31).positive?
86
90
  stream_dependency %= 2**31
87
- fragment = s[14..]
88
- return ConnectionError.new(ErrorCode::FRAME_SIZE_ERROR, 'invalid frame') if fragment.bytesize + 5 != payload_length
91
+ return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'cannot depend on itself') if stream_dependency == stream_id
92
+
93
+ fragment = io.get_string(5)
94
+ return ConnectionError.new(ErrorCode::FRAME_SIZE_ERROR, 'invalid frame') if fragment.bytesize + 5 != s.bytesize
89
95
  elsif padded
90
- pad_length = s[9].unpack1('C')
91
- fragment_length = payload_length - pad_length - 1
92
- fragment = s[10...10 + fragment_length]
93
- padding = s[10 + fragment_length..]
94
- return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if pad_length >= payload_length
96
+ io = IO::Buffer.for(s)
97
+ pad_length = io.get_value(:U8, 0)
98
+ fragment_length = s.bytesize - pad_length - 1
99
+ fragment = io.get_string(1, fragment_length)
100
+ padding = io.get_string(1 + fragment_length)
101
+ return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if pad_length >= s.bytesize
95
102
  return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if padding.bytesize != pad_length
96
103
  else
97
- fragment = s[9..]
98
- return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if fragment.bytesize != payload_length
104
+ fragment = s
99
105
  end
100
106
 
101
107
  Headers.new(end_headers, end_stream, stream_id, stream_dependency, weight, fragment, padding)
102
108
  end
103
109
  # rubocop: enable Metrics/AbcSize
104
110
  # rubocop: enable Metrics/CyclomaticComplexity
111
+ # rubocop: enable Metrics/MethodLength
105
112
  # rubocop: enable Metrics/PerceivedComplexity
106
113
  end
107
114
  end
@@ -32,17 +32,15 @@ module Biryani
32
32
  end
33
33
 
34
34
  # @param s [String]
35
+ # @param flags [Integer]
36
+ # @param stream_id [Integer]
35
37
  #
36
38
  # @return [Ping]
37
- def self.read(s)
38
- payload_length, _, flags, stream_id = Frame.read_header(s)
39
- return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if s[9..].bytesize != payload_length
40
- return ConnectionError.new(ErrorCode::FRAME_SIZE_ERROR, 'PING payload length MUST be 8') if s[9..].bytesize != 8
39
+ def self.read(s, flags, stream_id)
40
+ return ConnectionError.new(ErrorCode::FRAME_SIZE_ERROR, 'PING payload length MUST be 8') if s.bytesize != 8
41
41
 
42
42
  ack = Frame.read_ack(flags)
43
- opaque = s[9..]
44
-
45
- Ping.new(ack, stream_id, opaque)
43
+ Ping.new(ack, stream_id, s)
46
44
  end
47
45
  end
48
46
  end
@@ -27,15 +27,16 @@ module Biryani
27
27
  end
28
28
 
29
29
  # @param s [String]
30
+ # @param _flags [Integer]
31
+ # @param stream_id [Integer]
30
32
  #
31
33
  # @return [Priority]
32
- def self.read(s)
33
- payload_length, _, _, stream_id = Frame.read_header(s)
34
- return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if s[9..].bytesize != payload_length
35
- return ConnectionError.new(ErrorCode::FRAME_SIZE_ERROR, 'PRIORITY payload length MUST be 5') if payload_length != 5
34
+ def self.read(s, _flags, stream_id)
35
+ return ConnectionError.new(ErrorCode::FRAME_SIZE_ERROR, 'PRIORITY payload length MUST be 5') if s.bytesize != 5
36
36
 
37
- stream_dependency, weight = s[9..13].unpack('NC')
37
+ stream_dependency, weight = s.unpack('NC')
38
38
  stream_dependency %= 2**31
39
+ return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'cannot depend on itself') if stream_dependency == stream_id
39
40
 
40
41
  Priority.new(stream_id, stream_dependency, weight)
41
42
  end
@@ -43,33 +43,32 @@ module Biryani
43
43
  end
44
44
 
45
45
  # @param s [String]
46
+ # @param flags [Integer]
47
+ # @param stream_id [Integer]
46
48
  #
47
49
  # @return [PushPromise]
48
- # rubocop: disable Metrics/AbcSize
49
- def self.read(s)
50
- payload_length, _, flags, stream_id = Frame.read_header(s)
51
- return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if s[9..].bytesize != payload_length
52
-
50
+ def self.read(s, flags, stream_id)
53
51
  padded = Frame.read_padded(flags)
54
52
  end_headers = Frame.read_end_headers(flags)
55
53
 
54
+ io = IO::Buffer.for(s)
56
55
  if padded
57
- pad_length = s[9].unpack1('C')
58
- promised_stream_id = s[10..13].unpack1('N')
59
- fragment_length = payload_length - pad_length - 5
60
- fragment = s[14...14 + fragment_length]
61
- padding = s[14 + fragment_length..]
62
- return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if pad_length >= payload_length
56
+ pad_length, promised_stream_id = io.get_values(%i[U8 U32], 0)
57
+ promised_stream_id %= 2**31 # Promised Stream ID (31)
58
+ fragment_length = s.bytesize - pad_length - 5
59
+ fragment = io.get_string(5, fragment_length)
60
+ padding = io.get_string(5 + fragment_length)
61
+ return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if pad_length >= s.bytesize
63
62
  return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if padding.bytesize != pad_length
64
63
  else
65
- promised_stream_id = s[9..12].unpack1('N')
66
- fragment = s[13..]
67
- return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if fragment.bytesize + 4 != payload_length
64
+ promised_stream_id = io.get_value(:U32, 0)
65
+ promised_stream_id %= 2**31 # Promised Stream ID (31)
66
+ fragment = io.get_string(4)
67
+ return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if fragment.bytesize + 4 != s.bytesize
68
68
  end
69
69
 
70
70
  PushPromise.new(end_headers, stream_id, promised_stream_id, fragment, padding)
71
71
  end
72
- # rubocop: enable Metrics/AbcSize
73
72
  end
74
73
  end
75
74
  end
@@ -25,14 +25,14 @@ module Biryani
25
25
  end
26
26
 
27
27
  # @param s [String]
28
+ # @param _flags [Integer]
29
+ # @param stream_id [Integer]
28
30
  #
29
31
  # @return [RstStream]
30
- def self.read(s)
31
- payload_length, _, _, stream_id = Frame.read_header(s)
32
- return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if s[9..].bytesize != payload_length
33
- return ConnectionError.new(ErrorCode::FRAME_SIZE_ERROR, 'RST_STREAM payload length MUST be 4') if s[9..].bytesize != 4
32
+ def self.read(s, _flags, stream_id)
33
+ return ConnectionError.new(ErrorCode::FRAME_SIZE_ERROR, 'RST_STREAM payload length MUST be 4') if s.bytesize != 4
34
34
 
35
- error_code = s[9..].unpack1('N')
35
+ error_code = s.unpack1('N')
36
36
  RstStream.new(stream_id, error_code)
37
37
  end
38
38
  end
@@ -33,18 +33,18 @@ module Biryani
33
33
  end
34
34
 
35
35
  # @param s [String]
36
+ # @param flags [Integer]
37
+ # @param stream_id [Integer]
36
38
  #
37
39
  # @return [Settings]
38
40
  # rubocop: disable Metrics/AbcSize
39
41
  # rubocop: disable Metrics/CyclomaticComplexity
40
42
  # rubocop: disable Metrics/PerceivedComplexity
41
- def self.read(s)
42
- payload_length, _, flags, stream_id = Frame.read_header(s)
43
- return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if s[9..].bytesize != payload_length
44
- return ConnectionError.new(ErrorCode::FRAME_SIZE_ERROR, 'SETTINGS payload length MUST be a multiple of 6') if payload_length % 6 != 0
43
+ def self.read(s, flags, stream_id)
44
+ return ConnectionError.new(ErrorCode::FRAME_SIZE_ERROR, 'SETTINGS payload length MUST be a multiple of 6') if s.bytesize % 6 != 0
45
45
 
46
46
  ack = Frame.read_ack(flags)
47
- setting = s[9..].unpack('nN' * (payload_length / 6)).each_slice(2).to_h
47
+ setting = s.unpack('nN' * (s.bytesize / 6)).each_slice(2).to_h
48
48
  return ConnectionError.new(ErrorCode::FRAME_SIZE_ERROR, 'SETTINGS MUST NOT have setting with ack') \
49
49
  if ack && setting.any?
50
50
  return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid SETTINGS_ENABLE_PUSH') \
@@ -21,21 +21,7 @@ module Biryani
21
21
 
22
22
  # @return [String]
23
23
  def to_binary_s
24
- payload_length = length
25
-
26
- Frame.to_binary_s_header(payload_length, @f_type, flags, @stream_id) + @payload
27
- end
28
-
29
- # @param s [String]
30
- #
31
- # @return [Unknown]
32
- def self.read(s)
33
- payload_length, f_type, flags, stream_id = Frame.read_header(s)
34
- return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if s[9..].bytesize != payload_length
35
-
36
- payload = s[9..]
37
-
38
- Unknown.new(f_type, flags, stream_id, payload)
24
+ Frame.to_binary_s_header(length, @f_type, @flags, @stream_id) + @payload
39
25
  end
40
26
  end
41
27
  end
@@ -25,14 +25,14 @@ module Biryani
25
25
  end
26
26
 
27
27
  # @param s [String]
28
+ # @param _flags [Integer]
29
+ # @param stream_id [Integer]
28
30
  #
29
31
  # @return [WindowUpdate]
30
- def self.read(s)
31
- payload_length, _, _, stream_id = Frame.read_header(s)
32
- return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if s[9..].bytesize != payload_length
33
- return ConnectionError.new(ErrorCode::FRAME_SIZE_ERROR, 'WINDOW_UPDATE payload length MUST be 4') if s[9..].bytesize != 4
32
+ def self.read(s, _flags, stream_id)
33
+ return ConnectionError.new(ErrorCode::FRAME_SIZE_ERROR, 'WINDOW_UPDATE payload length MUST be 4') if s.bytesize != 4
34
34
 
35
- window_size_increment = s[9..].unpack1('N')
35
+ window_size_increment = s.unpack1('N')
36
36
  return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'WINDOW_UPDATE invalid window size increment 0') if window_size_increment.zero?
37
37
  return ConnectionError.new(ErrorCode::FLOW_CONTROL_ERROR, 'WINDOW_UPDATE invalid window size increment greater than 2^31-1') if window_size_increment > 2**31 - 1
38
38
 
data/lib/biryani/frame.rb CHANGED
@@ -10,6 +10,11 @@ module Biryani
10
10
  GOAWAY = 0x07
11
11
  WINDOW_UPDATE = 0x08
12
12
  CONTINUATION = 0x09
13
+
14
+ # @return [Boolean]
15
+ def self.unknown?(typ)
16
+ typ.negative? || typ > 0x09
17
+ end
13
18
  end
14
19
 
15
20
  module ErrorCode
@@ -37,11 +42,11 @@ module Biryani
37
42
  # @return [Integer]
38
43
  # @return [Integer]
39
44
  def self.read_header(s)
40
- b0, b1, b2, f_type, uint8 = s[0..4].bytes
45
+ b0, b1, b2, f_type, flags, stream_id = s.unpack('CCCCCN')
41
46
  payload_length = (b0 << 16) | (b1 << 8) | b2
42
- stream_id = s[5..8].unpack1('N') % 2**31 # Stream Identifier (31)
47
+ stream_id %= 2**31 # Stream Identifier (31)
43
48
 
44
- [payload_length, f_type, uint8, stream_id]
49
+ [payload_length, f_type, flags, stream_id]
45
50
  end
46
51
 
47
52
  # @param uint8 [Integer]
@@ -136,9 +141,10 @@ module Biryani
136
141
 
137
142
  payload_length, f_type, flags, stream_id = read_header(s)
138
143
  payload = io.read(payload_length)
144
+ return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if payload.bytesize != payload_length
139
145
  return Frame::Unknown.new(f_type, flags, stream_id, payload) unless FRAME_MAP.key?(f_type)
140
146
 
141
- FRAME_MAP[f_type].read(s + payload)
147
+ FRAME_MAP[f_type].read(payload, flags, stream_id)
142
148
  rescue StandardError
143
149
  ConnectionError.new(ErrorCode::FRAME_SIZE_ERROR, 'invalid frame')
144
150
  end