plum 0.0.1

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 (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.travis.yml +14 -0
  4. data/Gemfile +4 -0
  5. data/Guardfile +7 -0
  6. data/LICENSE +21 -0
  7. data/README.md +14 -0
  8. data/Rakefile +12 -0
  9. data/bin/.gitkeep +0 -0
  10. data/examples/local_server.rb +206 -0
  11. data/examples/static_server.rb +157 -0
  12. data/lib/plum.rb +21 -0
  13. data/lib/plum/binary_string.rb +74 -0
  14. data/lib/plum/connection.rb +201 -0
  15. data/lib/plum/connection_utils.rb +38 -0
  16. data/lib/plum/errors.rb +35 -0
  17. data/lib/plum/event_emitter.rb +19 -0
  18. data/lib/plum/flow_control.rb +97 -0
  19. data/lib/plum/frame.rb +163 -0
  20. data/lib/plum/frame_factory.rb +53 -0
  21. data/lib/plum/frame_utils.rb +50 -0
  22. data/lib/plum/hpack/constants.rb +331 -0
  23. data/lib/plum/hpack/context.rb +55 -0
  24. data/lib/plum/hpack/decoder.rb +145 -0
  25. data/lib/plum/hpack/encoder.rb +105 -0
  26. data/lib/plum/hpack/huffman.rb +42 -0
  27. data/lib/plum/http_connection.rb +33 -0
  28. data/lib/plum/https_connection.rb +24 -0
  29. data/lib/plum/stream.rb +217 -0
  30. data/lib/plum/stream_utils.rb +58 -0
  31. data/lib/plum/version.rb +3 -0
  32. data/plum.gemspec +29 -0
  33. data/test/plum/connection/test_handle_frame.rb +70 -0
  34. data/test/plum/hpack/test_context.rb +63 -0
  35. data/test/plum/hpack/test_decoder.rb +291 -0
  36. data/test/plum/hpack/test_encoder.rb +49 -0
  37. data/test/plum/hpack/test_huffman.rb +36 -0
  38. data/test/plum/stream/test_handle_frame.rb +262 -0
  39. data/test/plum/test_binary_string.rb +64 -0
  40. data/test/plum/test_connection.rb +96 -0
  41. data/test/plum/test_connection_utils.rb +29 -0
  42. data/test/plum/test_error.rb +13 -0
  43. data/test/plum/test_flow_control.rb +167 -0
  44. data/test/plum/test_frame.rb +59 -0
  45. data/test/plum/test_frame_factory.rb +56 -0
  46. data/test/plum/test_frame_utils.rb +46 -0
  47. data/test/plum/test_https_connection.rb +37 -0
  48. data/test/plum/test_stream.rb +32 -0
  49. data/test/plum/test_stream_utils.rb +16 -0
  50. data/test/server.crt +19 -0
  51. data/test/server.csr +16 -0
  52. data/test/server.key +27 -0
  53. data/test/test_helper.rb +28 -0
  54. data/test/utils/assertions.rb +60 -0
  55. data/test/utils/server.rb +63 -0
  56. metadata +234 -0
@@ -0,0 +1,58 @@
1
+ using Plum::BinaryString
2
+
3
+ module Plum
4
+ module StreamUtils
5
+ # Responds to HTTP request.
6
+ #
7
+ # @param headers [Hash<String, String>] The response headers.
8
+ # @param body [String, IO] The response body.
9
+ def respond(headers, body = nil, end_stream: true) # TODO: priority, padding
10
+ if body
11
+ send_headers(headers, end_stream: false)
12
+ send_data(body, end_stream: end_stream)
13
+ else
14
+ send_headers(headers, end_stream: end_stream)
15
+ end
16
+ end
17
+
18
+ # Reserves a stream to server push. Sends PUSH_PROMISE and create new stream.
19
+ #
20
+ # @param headers [Hash<String, String>] The *request* headers. It must contain all of them: ':authority', ':method', ':scheme' and ':path'.
21
+ # @return [Stream] The stream to send push response.
22
+ def promise(headers)
23
+ stream = @connection.reserve_stream(weight: self.weight + 1, parent: self)
24
+ encoded = @connection.hpack_encoder.encode(headers)
25
+ original = Frame.push_promise(id, stream.id, encoded, :end_headers)
26
+ original.split_headers(@connection.remote_settings[:max_frame_size]).each do |frame|
27
+ send frame
28
+ end
29
+ stream
30
+ end
31
+
32
+ private
33
+ def send_headers(headers, end_stream:)
34
+ max = @connection.remote_settings[:max_frame_size]
35
+ encoded = @connection.hpack_encoder.encode(headers)
36
+ original_frame = Frame.headers(id, encoded, :end_headers, (end_stream && :end_stream || nil))
37
+ original_frame.split_headers(max).each do |frame|
38
+ send frame
39
+ end
40
+ @state = :half_closed_local if end_stream
41
+ end
42
+
43
+ def send_data(data, end_stream: true)
44
+ max = @connection.remote_settings[:max_frame_size]
45
+ if data.is_a?(IO)
46
+ while !data.eof? && fragment = data.readpartial(max)
47
+ send Frame.data(id, fragment, (end_stream && data.eof? && :end_stream))
48
+ end
49
+ else
50
+ original = Frame.data(id, data, (end_stream && :end_stream))
51
+ original.split_data(max).each do |frame|
52
+ send frame
53
+ end
54
+ end
55
+ @state = :half_closed_local if end_stream
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,3 @@
1
+ module Plum
2
+ VERSION = "0.0.1"
3
+ end
data/plum.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "plum/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "plum"
7
+ spec.version = Plum::VERSION
8
+ spec.authors = ["rhenium"]
9
+ spec.email = ["k@rhe.jp"]
10
+
11
+ spec.summary = %q{A minimal implementation of HTTP/2 server.}
12
+ spec.description = spec.summary
13
+ spec.homepage = "https://github.com/rhenium/plum"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x00")
17
+ spec.executables = spec.files.grep(%r{^bin/[^.]}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^test/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.10"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "yard"
24
+ spec.add_development_dependency "minitest", "~> 5.7.0"
25
+ spec.add_development_dependency "simplecov"
26
+ spec.add_development_dependency "codeclimate-test-reporter"
27
+ spec.add_development_dependency "guard"
28
+ spec.add_development_dependency "guard-minitest"
29
+ end
@@ -0,0 +1,70 @@
1
+ require "test_helper"
2
+
3
+ using Plum::BinaryString
4
+
5
+ class ServerConnectionHandleFrameTest < Minitest::Test
6
+ ## SETTINGS
7
+ def test_server_handle_settings
8
+ open_server_connection {|con|
9
+ assert_equal(4096, con.remote_settings[:header_table_size])
10
+ con << Frame.new(type: :settings, stream_id: 0, payload: "\x00\x01\x00\x00\x10\x10").assemble
11
+ assert_equal(0x1010, con.remote_settings[:header_table_size])
12
+ }
13
+ end
14
+
15
+ def test_server_handle_settings
16
+ open_server_connection {|con|
17
+ assert_no_error {
18
+ con << Frame.new(type: :settings, stream_id: 0, flags: [:ack], payload: "").assemble
19
+ }
20
+ assert_connection_error(:frame_size_error) {
21
+ con << Frame.new(type: :settings, stream_id: 0, flags: [:ack], payload: "\x00").assemble
22
+ }
23
+ }
24
+ end
25
+
26
+ def test_server_handle_settings_invalid
27
+ open_server_connection {|con|
28
+ assert_no_error {
29
+ con << Frame.new(type: :settings, stream_id: 0, payload: "\xff\x01\x00\x00\x10\x10").assemble
30
+ }
31
+ }
32
+ end
33
+
34
+ ## PING
35
+ def test_server_handle_ping
36
+ open_server_connection {|con|
37
+ con << Frame.new(type: :ping, flags: [], stream_id: 0, payload: "AAAAAAAA").assemble
38
+ last = sent_frames.last
39
+ assert_equal(:ping, last.type)
40
+ assert_equal([:ack], last.flags)
41
+ assert_equal("AAAAAAAA", last.payload)
42
+ }
43
+ end
44
+
45
+ def test_server_handle_ping_error
46
+ open_server_connection {|con|
47
+ assert_connection_error(:frame_size_error) {
48
+ con << Frame.new(type: :ping, stream_id: 0, payload: "A" * 7).assemble
49
+ }
50
+ }
51
+ end
52
+
53
+ def test_server_handle_ping_ack
54
+ open_server_connection {|con|
55
+ con << Frame.new(type: :ping, flags: [:ack], stream_id: 0, payload: "A" * 8).assemble
56
+ last = sent_frames.last
57
+ refute_equal(:ping, last.type) if last
58
+ }
59
+ end
60
+
61
+ ## GOAWAY
62
+ def test_server_handle_goaway_reply
63
+ open_server_connection {|con|
64
+ assert_no_error {
65
+ con << Frame.goaway(1234, :stream_closed).assemble
66
+ }
67
+ assert_equal(:goaway, sent_frames.last.type)
68
+ }
69
+ end
70
+ end
@@ -0,0 +1,63 @@
1
+ require "test_helper"
2
+
3
+ class HPACKContextTest < Minitest::Test
4
+ def test_store
5
+ context = new_context
6
+ context.store("あああ", "いい")
7
+ assert_equal([["あああ", "いい"]], context.dynamic_table)
8
+ assert_equal("あああいい".bytesize + 32, context.size)
9
+ end
10
+
11
+ def test_store_eviction
12
+ context = new_context(1)
13
+ context.store("あああ", "いい")
14
+ assert_equal([], context.dynamic_table)
15
+ assert_equal(0, context.size)
16
+ end
17
+
18
+ def test_fetch_static
19
+ context = new_context
20
+ assert_equal([":method", "POST"], context.fetch(3))
21
+ end
22
+
23
+ def test_fetch_dynamic
24
+ context = new_context
25
+ context.store("あああ", "いい")
26
+ assert_equal(["あああ", "いい"], context.fetch(62))
27
+ end
28
+
29
+ def test_fetch_error
30
+ context = new_context
31
+ context.store("あああ", "いい")
32
+ assert_raises(Plum::HPACKError) {
33
+ context.fetch(64)
34
+ }
35
+ end
36
+
37
+ def test_search_static
38
+ context = new_context
39
+ i1 = context.search(":method", "POST")
40
+ assert_equal(3, i1)
41
+ end
42
+
43
+ def test_search_dynamic
44
+ context = new_context
45
+ context.store("あああ", "abc")
46
+ context.store("あああ", "いい")
47
+ i1 = context.search("あああ", "abc")
48
+ assert_equal(63, i1)
49
+ i2 = context.search("あああ", "AAA")
50
+ assert_equal(nil, i2)
51
+ i3 = context.search("あああ", nil)
52
+ assert_equal(62, i3)
53
+ end
54
+
55
+ private
56
+ def new_context(limit = 1 << 31)
57
+ @c ||= Class.new {
58
+ include Plum::HPACK::Context
59
+ public *Plum::HPACK::Context.private_instance_methods
60
+ }
61
+ @c.new(limit)
62
+ end
63
+ end
@@ -0,0 +1,291 @@
1
+ require "test_helper"
2
+
3
+ class HPACKDecoderTest < Minitest::Test
4
+ # C.1.1
5
+ def test_hpack_read_integer_small
6
+ buf = [0b11001010, 0b00001111].pack("C*")
7
+ result = new_decoder.__send__(:read_integer!, buf, 5)
8
+ assert_equal(10, result)
9
+ assert_equal([0b00001111].pack("C*"), buf)
10
+ end
11
+
12
+ # C.1.2
13
+ def test_hpack_read_integer_big
14
+ buf = [0b11011111, 0b10011010, 0b00001010, 0b00001111].pack("C*")
15
+ result = new_decoder.__send__(:read_integer!, buf, 5)
16
+ assert_equal(1337, result)
17
+ assert_equal([0b00001111].pack("C*"), buf)
18
+ end
19
+
20
+ # C.1.3
21
+ def test_hpack_read_integer_8prefix
22
+ buf = [0b00101010, 0b00001111].pack("C*")
23
+ result = new_decoder.__send__(:read_integer!, buf, 8)
24
+ assert_equal(42, result)
25
+ assert_equal([0b00001111].pack("C*"), buf)
26
+ end
27
+
28
+ def test_hpack_read_integer_too_big
29
+ buf = [0b11011111, 0b10011010, 0b10001010, 0b10001111, 0b11111111, 0b00000011].pack("C*")
30
+ assert_raises(HPACKError) {
31
+ new_decoder.__send__(:read_integer!, buf, 5)
32
+ }
33
+ end
34
+
35
+ def test_hpack_read_integer_incomplete
36
+ buf = [0b11011111, 0b10011010].pack("C*")
37
+ assert_raises(HPACKError) {
38
+ new_decoder.__send__(:read_integer!, buf, 5)
39
+ }
40
+ end
41
+
42
+ # C.2.1
43
+ def test_hpack_decode_indexing
44
+ encoded = "\x40\x0acustom-key\x0dcustom-header"
45
+ result = new_decoder.decode(encoded)
46
+ assert_equal([["custom-key", "custom-header"]], result)
47
+ end
48
+
49
+ def test_hpack_decode_indexing_imcomplete
50
+ encoded = "\x40\x0acus"
51
+ assert_raises(HTTPError) {
52
+ new_decoder.decode(encoded)
53
+ }
54
+ end
55
+
56
+ # C.2.2
57
+ def test_hpack_decode_without_indexing
58
+ encoded = "\x04\x0c/sample/path"
59
+ result = new_decoder.decode(encoded)
60
+ assert_equal([[":path", "/sample/path"]], result)
61
+ end
62
+
63
+ # C.2.3
64
+ def test_hpack_decode_without_indexing2
65
+ encoded = "\x10\x08password\x06secret"
66
+ result = new_decoder.decode(encoded)
67
+ assert_equal([["password", "secret"]], result)
68
+ end
69
+
70
+ # C.2.4
71
+ def test_hpack_decode_index
72
+ encoded = "\x82"
73
+ result = new_decoder.decode(encoded)
74
+ assert_equal([[":method", "GET"]], result)
75
+ end
76
+
77
+ # C.3.1
78
+ def test_hpack_decode_headers_without_huffman
79
+ decoder = new_decoder
80
+ encoded = "\x82\x86\x84\x41\x0f\x77\x77\x77\x2e\x65\x78\x61\x6d\x70\x6c\x65\x2e\x63\x6f\x6d"
81
+ result = decoder.decode(encoded)
82
+ expected = [
83
+ [":method", "GET"],
84
+ [":scheme", "http"],
85
+ [":path", "/"],
86
+ [":authority", "www.example.com"]
87
+ ]
88
+ assert_equal(expected, result)
89
+
90
+ decoder # for C.3.2
91
+ end
92
+
93
+ # C.3.2
94
+ def test_hpack_decode_headers_without_huffman2
95
+ decoder = test_hpack_decode_headers_without_huffman
96
+ encoded = "\x82\x86\x84\xbe\x58\x08\x6e\x6f\x2d\x63\x61\x63\x68\x65"
97
+ result = decoder.decode(encoded)
98
+ expected = [
99
+ [":method", "GET"],
100
+ [":scheme", "http"],
101
+ [":path", "/"],
102
+ [":authority", "www.example.com"],
103
+ ["cache-control", "no-cache"],
104
+ ]
105
+ assert_equal(expected, result)
106
+
107
+ decoder # for C.3.3
108
+ end
109
+
110
+ # C.3.3
111
+ def test_hpack_decode_headers_without_huffman3
112
+ decoder = test_hpack_decode_headers_without_huffman2
113
+ encoded = "\x82\x87\x85\xbf\x40\x0a\x63\x75\x73\x74\x6f\x6d\x2d\x6b\x65\x79\x0c\x63\x75\x73\x74\x6f\x6d\x2d\x76\x61\x6c\x75\x65"
114
+ result = decoder.decode(encoded)
115
+ expected = [
116
+ [":method", "GET"],
117
+ [":scheme", "https"],
118
+ [":path", "/index.html"],
119
+ [":authority", "www.example.com"],
120
+ ["custom-key", "custom-value"],
121
+ ]
122
+ assert_equal(expected, result)
123
+ end
124
+
125
+ # C.4.1
126
+ def test_hpack_decode_headers_with_huffman
127
+ decoder = new_decoder
128
+ encoded = "\x82\x86\x84\x41\x8c\xf1\xe3\xc2\xe5\xf2\x3a\x6b\xa0\xab\x90\xf4\xff"
129
+ result = decoder.decode(encoded)
130
+ expected = [
131
+ [":method", "GET"],
132
+ [":scheme", "http"],
133
+ [":path", "/"],
134
+ [":authority", "www.example.com"]
135
+ ]
136
+ assert_equal(expected, result)
137
+
138
+ decoder # for C.4.2
139
+ end
140
+
141
+ # C.4.2
142
+ def test_hpack_decode_headers_with_huffman2
143
+ decoder = test_hpack_decode_headers_with_huffman
144
+ encoded = "\x82\x86\x84\xbe\x58\x86\xa8\xeb\x10\x64\x9c\xbf"
145
+ result = decoder.decode(encoded)
146
+ expected = [
147
+ [":method", "GET"],
148
+ [":scheme", "http"],
149
+ [":path", "/"],
150
+ [":authority", "www.example.com"],
151
+ ["cache-control", "no-cache"],
152
+ ]
153
+ assert_equal(expected, result)
154
+
155
+ decoder # for C.4.3
156
+ end
157
+
158
+ # C.4.3
159
+ def test_hpack_decode_headers_with_huffman3
160
+ decoder = test_hpack_decode_headers_with_huffman2
161
+ encoded = "\x82\x87\x85\xbf\x40\x88\x25\xa8\x49\xe9\x5b\xa9\x7d\x7f\x89\x25\xa8\x49\xe9\x5b\xb8\xe8\xb4\xbf"
162
+ result = decoder.decode(encoded)
163
+ expected = [
164
+ [":method", "GET"],
165
+ [":scheme", "https"],
166
+ [":path", "/index.html"],
167
+ [":authority", "www.example.com"],
168
+ ["custom-key", "custom-value"],
169
+ ]
170
+ assert_equal(expected, result)
171
+ end
172
+
173
+ # C.5.1
174
+ def test_hpack_decode_response_without_huffman
175
+ decoder = new_decoder(256)
176
+ encoded = "\x48\x03\x33\x30\x32\x58\x07\x70\x72\x69\x76\x61\x74\x65\x61\x1d\x4d\x6f\x6e\x2c\x20\x32\x31\x20\x4f\x63\x74\x20\x32\x30\x31\x33\x20\x32\x30\x3a\x31\x33\x3a\x32\x31\x20\x47\x4d\x54\x6e\x17\x68\x74\x74\x70\x73\x3a\x2f\x2f\x77\x77\x77\x2e\x65\x78\x61\x6d\x70\x6c\x65\x2e\x63\x6f\x6d"
177
+ result = decoder.decode(encoded)
178
+ expected = [
179
+ [":status", "302"],
180
+ ["cache-control", "private"],
181
+ ["date", "Mon, 21 Oct 2013 20:13:21 GMT"],
182
+ ["location", "https://www.example.com"]
183
+ ]
184
+ assert_equal(expected, result)
185
+
186
+ decoder # for C.5.2
187
+ end
188
+
189
+ # C.5.2
190
+ def test_hpack_decode_response_without_huffman2
191
+ decoder = test_hpack_decode_response_without_huffman
192
+ encoded = "\x48\x03\x33\x30\x37\xc1\xc0\xbf"
193
+ result = decoder.decode(encoded)
194
+ expected = [
195
+ [":status", "307"],
196
+ ["cache-control", "private"],
197
+ ["date", "Mon, 21 Oct 2013 20:13:21 GMT"],
198
+ ["location", "https://www.example.com"]
199
+ ]
200
+ assert_equal(expected, result)
201
+ refute_includes(decoder.dynamic_table, [":status", "302"]) # evicted
202
+
203
+ decoder # for C.5.3
204
+ end
205
+
206
+ # C.5.3
207
+ def test_hpack_decode_response_without_huffman3
208
+ decoder = test_hpack_decode_response_without_huffman2
209
+ encoded = "\x88\xc1\x61\x1d\x4d\x6f\x6e\x2c\x20\x32\x31\x20\x4f\x63\x74\x20\x32\x30\x31\x33\x20\x32\x30\x3a\x31\x33\x3a\x32\x32\x20\x47\x4d\x54\xc0\x5a\x04\x67\x7a\x69\x70\x77\x38\x66\x6f\x6f\x3d\x41\x53\x44\x4a\x4b\x48\x51\x4b\x42\x5a\x58\x4f\x51\x57\x45\x4f\x50\x49\x55\x41\x58\x51\x57\x45\x4f\x49\x55\x3b\x20\x6d\x61\x78\x2d\x61\x67\x65\x3d\x33\x36\x30\x30\x3b\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x31"
210
+ result = decoder.decode(encoded)
211
+ expected = [
212
+ [":status", "200"],
213
+ ["cache-control", "private"],
214
+ ["date", "Mon, 21 Oct 2013 20:13:22 GMT"],
215
+ ["location", "https://www.example.com"],
216
+ ["content-encoding", "gzip"],
217
+ ["set-cookie", "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"]
218
+ ]
219
+ assert_equal(expected, result)
220
+ end
221
+
222
+ # C.6.1
223
+ def test_hpack_decode_response_with_huffman
224
+ decoder = new_decoder(256)
225
+ encoded = "\x48\x82\x64\x02\x58\x85\xae\xc3\x77\x1a\x4b\x61\x96\xd0\x7a\xbe\x94\x10\x54\xd4\x44\xa8\x20\x05\x95\x04\x0b\x81\x66\xe0\x82\xa6\x2d\x1b\xff\x6e\x91\x9d\x29\xad\x17\x18\x63\xc7\x8f\x0b\x97\xc8\xe9\xae\x82\xae\x43\xd3"
226
+ result = decoder.decode(encoded)
227
+ expected = [
228
+ [":status", "302"],
229
+ ["cache-control", "private"],
230
+ ["date", "Mon, 21 Oct 2013 20:13:21 GMT"],
231
+ ["location", "https://www.example.com"]
232
+ ]
233
+ assert_equal(expected, result)
234
+
235
+ decoder # for C.6.2
236
+ end
237
+
238
+ # C.6.2
239
+ def test_hpack_decode_response_with_huffman2
240
+ decoder = test_hpack_decode_response_with_huffman
241
+ encoded = "\x48\x83\x64\x0e\xff\xc1\xc0\xbf"
242
+ result = decoder.decode(encoded)
243
+ expected = [
244
+ [":status", "307"],
245
+ ["cache-control", "private"],
246
+ ["date", "Mon, 21 Oct 2013 20:13:21 GMT"],
247
+ ["location", "https://www.example.com"]
248
+ ]
249
+ assert_equal(expected, result)
250
+ refute_includes(decoder.dynamic_table, [":status", "302"]) # evicted
251
+
252
+ decoder # for C.6.3
253
+ end
254
+
255
+ # C.6.3
256
+ def test_hpack_decode_response_with_huffman3
257
+ decoder = test_hpack_decode_response_with_huffman2
258
+ encoded = "\x88\xc1\x61\x96\xd0\x7a\xbe\x94\x10\x54\xd4\x44\xa8\x20\x05\x95\x04\x0b\x81\x66\xe0\x84\xa6\x2d\x1b\xff\xc0\x5a\x83\x9b\xd9\xab\x77\xad\x94\xe7\x82\x1d\xd7\xf2\xe6\xc7\xb3\x35\xdf\xdf\xcd\x5b\x39\x60\xd5\xaf\x27\x08\x7f\x36\x72\xc1\xab\x27\x0f\xb5\x29\x1f\x95\x87\x31\x60\x65\xc0\x03\xed\x4e\xe5\xb1\x06\x3d\x50\x07"
259
+ result = decoder.decode(encoded)
260
+ expected = [
261
+ [":status", "200"],
262
+ ["cache-control", "private"],
263
+ ["date", "Mon, 21 Oct 2013 20:13:22 GMT"],
264
+ ["location", "https://www.example.com"],
265
+ ["content-encoding", "gzip"],
266
+ ["set-cookie", "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"]
267
+ ]
268
+ assert_equal(expected, result)
269
+ end
270
+
271
+ def test_hpack_decode_index_zero
272
+ decoder = new_decoder(256)
273
+ encoded = "\x80"
274
+ assert_raises(HPACKError) {
275
+ decoder.decode(encoded)
276
+ }
277
+ end
278
+
279
+ def test_hpack_decode_changing_limit
280
+ decoder = new_decoder(256)
281
+ encoded = "\x34"
282
+ assert_equal(256, decoder.limit)
283
+ decoder.decode(encoded)
284
+ assert_equal(0b00010100, decoder.limit)
285
+ end
286
+
287
+ private
288
+ def new_decoder(settings_header_table_size = 1 << 31)
289
+ Plum::HPACK::Decoder.new(settings_header_table_size)
290
+ end
291
+ end