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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.travis.yml +14 -0
- data/Gemfile +4 -0
- data/Guardfile +7 -0
- data/LICENSE +21 -0
- data/README.md +14 -0
- data/Rakefile +12 -0
- data/bin/.gitkeep +0 -0
- data/examples/local_server.rb +206 -0
- data/examples/static_server.rb +157 -0
- data/lib/plum.rb +21 -0
- data/lib/plum/binary_string.rb +74 -0
- data/lib/plum/connection.rb +201 -0
- data/lib/plum/connection_utils.rb +38 -0
- data/lib/plum/errors.rb +35 -0
- data/lib/plum/event_emitter.rb +19 -0
- data/lib/plum/flow_control.rb +97 -0
- data/lib/plum/frame.rb +163 -0
- data/lib/plum/frame_factory.rb +53 -0
- data/lib/plum/frame_utils.rb +50 -0
- data/lib/plum/hpack/constants.rb +331 -0
- data/lib/plum/hpack/context.rb +55 -0
- data/lib/plum/hpack/decoder.rb +145 -0
- data/lib/plum/hpack/encoder.rb +105 -0
- data/lib/plum/hpack/huffman.rb +42 -0
- data/lib/plum/http_connection.rb +33 -0
- data/lib/plum/https_connection.rb +24 -0
- data/lib/plum/stream.rb +217 -0
- data/lib/plum/stream_utils.rb +58 -0
- data/lib/plum/version.rb +3 -0
- data/plum.gemspec +29 -0
- data/test/plum/connection/test_handle_frame.rb +70 -0
- data/test/plum/hpack/test_context.rb +63 -0
- data/test/plum/hpack/test_decoder.rb +291 -0
- data/test/plum/hpack/test_encoder.rb +49 -0
- data/test/plum/hpack/test_huffman.rb +36 -0
- data/test/plum/stream/test_handle_frame.rb +262 -0
- data/test/plum/test_binary_string.rb +64 -0
- data/test/plum/test_connection.rb +96 -0
- data/test/plum/test_connection_utils.rb +29 -0
- data/test/plum/test_error.rb +13 -0
- data/test/plum/test_flow_control.rb +167 -0
- data/test/plum/test_frame.rb +59 -0
- data/test/plum/test_frame_factory.rb +56 -0
- data/test/plum/test_frame_utils.rb +46 -0
- data/test/plum/test_https_connection.rb +37 -0
- data/test/plum/test_stream.rb +32 -0
- data/test/plum/test_stream_utils.rb +16 -0
- data/test/server.crt +19 -0
- data/test/server.csr +16 -0
- data/test/server.key +27 -0
- data/test/test_helper.rb +28 -0
- data/test/utils/assertions.rb +60 -0
- data/test/utils/server.rb +63 -0
- metadata +234 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
using BinaryString
|
4
|
+
|
5
|
+
class ServerConnectionUtilsTest < Minitest::Test
|
6
|
+
def test_server_ping
|
7
|
+
open_server_connection {|con|
|
8
|
+
con.ping("ABCABCAB")
|
9
|
+
|
10
|
+
last = sent_frames.last
|
11
|
+
assert_equal(:ping, last.type)
|
12
|
+
assert_equal([], last.flags)
|
13
|
+
assert_equal("ABCABCAB", last.payload)
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_server_goaway
|
18
|
+
open_server_connection {|con|
|
19
|
+
con << Frame.headers(3, "", :end_stream, :end_headers).assemble
|
20
|
+
con.goaway(:stream_closed)
|
21
|
+
|
22
|
+
last = sent_frames.last
|
23
|
+
assert_equal(:goaway, last.type)
|
24
|
+
assert_equal([], last.flags)
|
25
|
+
assert_equal(3, last.payload.uint32)
|
26
|
+
assert_equal(HTTPError::ERROR_CODES[:stream_closed], last.payload.uint32(4))
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class ErrorTest < Minitest::Test
|
4
|
+
def test_http_error_http2_error_code
|
5
|
+
test = -> klass {
|
6
|
+
e = klass.new(:cancel)
|
7
|
+
assert_equal(0x08, e.http2_error_code)
|
8
|
+
}
|
9
|
+
|
10
|
+
test.call ConnectionError
|
11
|
+
test.call StreamError
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
using BinaryString
|
4
|
+
|
5
|
+
class FlowControlTest < Minitest::Test
|
6
|
+
def test_flow_control_window_update_server
|
7
|
+
open_server_connection {|con|
|
8
|
+
before_ws = con.recv_remaining_window
|
9
|
+
con.window_update(500)
|
10
|
+
|
11
|
+
last = sent_frames.last
|
12
|
+
assert_equal(:window_update, last.type)
|
13
|
+
assert_equal(0, last.stream_id)
|
14
|
+
assert_equal(500, last.payload.uint32)
|
15
|
+
assert_equal(before_ws + 500, con.recv_remaining_window)
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_flow_control_window_update_stream
|
20
|
+
open_new_stream {|stream|
|
21
|
+
before_ws = stream.recv_remaining_window
|
22
|
+
stream.window_update(500)
|
23
|
+
|
24
|
+
last = sent_frames.last
|
25
|
+
assert_equal(:window_update, last.type)
|
26
|
+
assert_equal(stream.id, last.stream_id)
|
27
|
+
assert_equal(500, last.payload.uint32)
|
28
|
+
assert_equal(before_ws + 500, stream.recv_remaining_window)
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_flow_control_window_update_zero
|
33
|
+
open_new_stream {|stream|
|
34
|
+
assert_stream_error(:protocol_error) {
|
35
|
+
stream.receive_frame Frame.new(type: :window_update,
|
36
|
+
stream_id: stream.id,
|
37
|
+
payload: "".push_uint32(0))
|
38
|
+
}
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_flow_control_window_update_frame_size
|
43
|
+
open_new_stream {|stream|
|
44
|
+
assert_connection_error(:frame_size_error) {
|
45
|
+
stream.receive_frame Frame.new(type: :window_update,
|
46
|
+
stream_id: stream.id,
|
47
|
+
payload: "".push_uint16(0))
|
48
|
+
}
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_flow_control_dont_send_data_exceeding_send_window
|
53
|
+
open_new_stream {|stream|
|
54
|
+
con = stream.connection
|
55
|
+
con << Frame.new(type: :settings,
|
56
|
+
stream_id: 0,
|
57
|
+
payload: "".push_uint16(Frame::SETTINGS_TYPE[:initial_window_size])
|
58
|
+
.push_uint32(4*2+1)).assemble
|
59
|
+
# only extend stream window size
|
60
|
+
con << Frame.new(type: :window_update,
|
61
|
+
stream_id: stream.id,
|
62
|
+
payload: "".push_uint32(100)).assemble
|
63
|
+
10.times {|i|
|
64
|
+
stream.send Frame.new(type: :data,
|
65
|
+
stream_id: stream.id,
|
66
|
+
payload: "".push_uint32(i))
|
67
|
+
}
|
68
|
+
|
69
|
+
last = sent_frames.last
|
70
|
+
assert_equal(1, last.payload.uint32)
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_flow_control_dont_send_data_upto_updated_send_window
|
75
|
+
open_new_stream {|stream|
|
76
|
+
con = stream.connection
|
77
|
+
con << Frame.new(type: :settings,
|
78
|
+
stream_id: 0,
|
79
|
+
payload: "".push_uint16(Frame::SETTINGS_TYPE[:initial_window_size])
|
80
|
+
.push_uint32(4*2+1)).assemble
|
81
|
+
10.times {|i|
|
82
|
+
stream.send Frame.new(type: :data,
|
83
|
+
stream_id: stream.id,
|
84
|
+
payload: "".push_uint32(i))
|
85
|
+
}
|
86
|
+
# only extend stream window size
|
87
|
+
con << Frame.new(type: :window_update,
|
88
|
+
stream_id: stream.id,
|
89
|
+
payload: "".push_uint32(100)).assemble
|
90
|
+
# and extend connection window size
|
91
|
+
con << Frame.new(type: :window_update,
|
92
|
+
stream_id: 0,
|
93
|
+
payload: "".push_uint32(4*2+1)).assemble
|
94
|
+
|
95
|
+
last = sent_frames.last
|
96
|
+
assert_equal(3, last.payload.uint32)
|
97
|
+
}
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_flow_control_update_send_initial_window_size
|
101
|
+
open_new_stream {|stream|
|
102
|
+
con = stream.connection
|
103
|
+
con << Frame.new(type: :settings,
|
104
|
+
stream_id: 0,
|
105
|
+
payload: "".push_uint16(Frame::SETTINGS_TYPE[:initial_window_size])
|
106
|
+
.push_uint32(4*2+1)).assemble
|
107
|
+
10.times {|i|
|
108
|
+
stream.send Frame.new(type: :data,
|
109
|
+
stream_id: stream.id,
|
110
|
+
payload: "".push_uint32(i))
|
111
|
+
}
|
112
|
+
# only extend stream window size
|
113
|
+
con << Frame.new(type: :window_update,
|
114
|
+
stream_id: stream.id,
|
115
|
+
payload: "".push_uint32(100)).assemble
|
116
|
+
# and update initial window size
|
117
|
+
con << Frame.new(type: :settings,
|
118
|
+
stream_id: 0,
|
119
|
+
payload: "".push_uint16(Frame::SETTINGS_TYPE[:initial_window_size])
|
120
|
+
.push_uint32(4*4+1)).assemble
|
121
|
+
|
122
|
+
last = sent_frames.reverse.find {|f| f.type == :data }
|
123
|
+
assert_equal(3, last.payload.uint32)
|
124
|
+
}
|
125
|
+
end
|
126
|
+
|
127
|
+
def test_flow_control_recv_window_exceeded
|
128
|
+
prepare = ->(&blk) {
|
129
|
+
open_new_stream {|stream|
|
130
|
+
con = stream.connection
|
131
|
+
con.settings(initial_window_size: 24)
|
132
|
+
blk.call(con, stream)
|
133
|
+
}
|
134
|
+
}
|
135
|
+
|
136
|
+
prepare.call {|con, stream|
|
137
|
+
con.window_update(500) # extend only connection
|
138
|
+
con << Frame.headers(stream.id, "", :end_headers).assemble
|
139
|
+
assert_stream_error(:flow_control_error) {
|
140
|
+
con << Frame.data(stream.id, "\x00" * 30, :end_stream).assemble
|
141
|
+
}
|
142
|
+
}
|
143
|
+
|
144
|
+
prepare.call {|con, stream|
|
145
|
+
stream.window_update(500) # extend only stream
|
146
|
+
con << Frame.headers(stream.id, "", :end_headers).assemble
|
147
|
+
assert_connection_error(:flow_control_error) {
|
148
|
+
con << Frame.data(stream.id, "\x00" * 30, :end_stream).assemble
|
149
|
+
}
|
150
|
+
}
|
151
|
+
end
|
152
|
+
|
153
|
+
def test_flow_control_update_recv_initial_window_size
|
154
|
+
open_new_stream {|stream|
|
155
|
+
con = stream.connection
|
156
|
+
con.settings(initial_window_size: 24)
|
157
|
+
stream.window_update(1)
|
158
|
+
con << Frame.headers(stream.id, "", :end_headers).assemble
|
159
|
+
con << Frame.data(stream.id, "\x00" * 20, :end_stream).assemble
|
160
|
+
assert_equal(4, con.recv_remaining_window)
|
161
|
+
assert_equal(5, stream.recv_remaining_window)
|
162
|
+
con.settings(initial_window_size: 60)
|
163
|
+
assert_equal(40, con.recv_remaining_window)
|
164
|
+
assert_equal(41, stream.recv_remaining_window)
|
165
|
+
}
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class FrameTest < Minitest::Test
|
4
|
+
# Frame.parse!
|
5
|
+
def test_parse_header_uncomplete
|
6
|
+
buffer = "\x00\x00\x00" << "\x00" << "\x00"
|
7
|
+
buffer_orig = buffer.dup
|
8
|
+
assert_nil(Plum::Frame.parse!(buffer))
|
9
|
+
assert_equal(buffer_orig, buffer)
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_parse_body_uncomplete
|
13
|
+
buffer = "\x00\x00\x03" << "\x00" << "\x00" << "\x00\x00\x00\x00" << "ab"
|
14
|
+
buffer_orig = buffer.dup
|
15
|
+
assert_nil(Plum::Frame.parse!(buffer))
|
16
|
+
assert_equal(buffer_orig, buffer)
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_parse
|
20
|
+
# R 0x1, stream_id 0x4, body "abc"
|
21
|
+
buffer = "\x00\x00\x03" << "\x00" << "\x09" << "\x80\x00\x00\x04" << "abc" << "next_frame_data"
|
22
|
+
frame = Plum::Frame.parse!(buffer)
|
23
|
+
assert_equal(3, frame.length)
|
24
|
+
assert_equal(:data, frame.type)
|
25
|
+
assert_equal([:end_stream, :padded], frame.flags)
|
26
|
+
assert_equal(0x04, frame.stream_id)
|
27
|
+
assert_equal("abc", frame.payload)
|
28
|
+
assert_equal("next_frame_data", buffer)
|
29
|
+
assert_equal(true, frame.frozen?)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Frame#assemble
|
33
|
+
def test_assemble
|
34
|
+
frame = Plum::Frame.new(type: :push_promise, flags: [:end_headers, :padded], stream_id: 0x678, payload: "payl")
|
35
|
+
bin = "\x00\x00\x04" << "\x05" << "\x0c" << "\x00\x00\x06\x78" << "payl"
|
36
|
+
assert_equal(bin, frame.assemble)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Frame#generate
|
40
|
+
def test_new
|
41
|
+
frame = Plum::Frame.new(type: :data,
|
42
|
+
stream_id: 12345,
|
43
|
+
flags: [:end_stream, :padded],
|
44
|
+
payload: "ぺいろーど".encode(Encoding::UTF_8))
|
45
|
+
assert_equal("ぺいろーど", frame.payload)
|
46
|
+
assert_equal("ぺいろーど".bytesize, frame.length)
|
47
|
+
assert_equal(:data, frame.type) # DATA
|
48
|
+
assert_equal([:end_stream, :padded], frame.flags) # 0x01 | 0x08
|
49
|
+
assert_equal(12345, frame.stream_id)
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_inspect
|
53
|
+
frame = Plum::Frame.new(type: :data,
|
54
|
+
stream_id: 12345,
|
55
|
+
flags: [:end_stream, :padded],
|
56
|
+
payload: "ぺいろーど")
|
57
|
+
frame.inspect
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
using Plum::BinaryString
|
4
|
+
class FrameFactoryTest < Minitest::Test
|
5
|
+
def test_rst_stream
|
6
|
+
frame = Frame.rst_stream(123, :stream_closed)
|
7
|
+
assert_frame(frame,
|
8
|
+
type: :rst_stream,
|
9
|
+
stream_id: 123)
|
10
|
+
assert_equal(HTTPError::ERROR_CODES[:stream_closed], frame.payload.uint32)
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_goaway
|
14
|
+
frame = Frame.goaway(0x55, :stream_closed, "debug")
|
15
|
+
assert_frame(frame,
|
16
|
+
type: :goaway,
|
17
|
+
stream_id: 0,
|
18
|
+
payload: "\x00\x00\x00\x55\x00\x00\x00\x05debug")
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_settings
|
22
|
+
frame = Frame.settings(header_table_size: 0x1010)
|
23
|
+
assert_frame(frame,
|
24
|
+
type: :settings,
|
25
|
+
stream_id: 0,
|
26
|
+
flags: [],
|
27
|
+
payload: "\x00\x01\x00\x00\x10\x10")
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_settings_ack
|
31
|
+
frame = Frame.settings(:ack)
|
32
|
+
assert_frame(frame,
|
33
|
+
type: :settings,
|
34
|
+
stream_id: 0,
|
35
|
+
flags: [:ack],
|
36
|
+
payload: "")
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_ping
|
40
|
+
frame = Frame.ping("12345678")
|
41
|
+
assert_frame(frame,
|
42
|
+
type: :ping,
|
43
|
+
stream_id: 0,
|
44
|
+
flags: [],
|
45
|
+
payload: "12345678")
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_ping_ack
|
49
|
+
frame = Frame.ping(:ack, "12345678")
|
50
|
+
assert_frame(frame,
|
51
|
+
type: :ping,
|
52
|
+
stream_id: 0,
|
53
|
+
flags: [:ack],
|
54
|
+
payload: "12345678")
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class FrameUtilsTest < Minitest::Test
|
4
|
+
def test_frame_enough_short
|
5
|
+
frame = Frame.new(type: :data, stream_id: 1, payload: "123")
|
6
|
+
ret = frame.split_data(3)
|
7
|
+
assert_equal(1, ret.size)
|
8
|
+
assert_equal("123", ret.first.payload)
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_frame_unknown
|
12
|
+
frame = Frame.new(type: :settings, stream_id: 1, payload: "123")
|
13
|
+
assert_raises { frame.split_data(2) }
|
14
|
+
assert_raises { frame.split_headers(2) }
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_frame_data
|
18
|
+
frame = Frame.new(type: :data, flags: [:end_stream], stream_id: 1, payload: "12345")
|
19
|
+
ret = frame.split_data(3)
|
20
|
+
assert_equal(2, ret.size)
|
21
|
+
assert_equal("123", ret.first.payload)
|
22
|
+
assert_equal([], ret.first.flags)
|
23
|
+
assert_equal("45", ret.last.payload)
|
24
|
+
assert_equal([:end_stream], ret.last.flags)
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_frame_headers
|
28
|
+
frame = Frame.new(type: :headers, flags: [:priority, :end_stream, :end_headers], stream_id: 1, payload: "1234567")
|
29
|
+
ret = frame.split_headers(3)
|
30
|
+
assert_equal(3, ret.size)
|
31
|
+
assert_equal("123", ret[0].payload)
|
32
|
+
assert_equal([:end_stream, :priority], ret[0].flags)
|
33
|
+
assert_equal("456", ret[1].payload)
|
34
|
+
assert_equal([], ret[1].flags)
|
35
|
+
assert_equal("7", ret[2].payload)
|
36
|
+
assert_equal([:end_headers], ret[2].flags)
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_frame_parse_settings
|
40
|
+
# :header_table_size => 0x1010, :enable_push => 0x00, :header_table_size => 0x1011 (overwrite)
|
41
|
+
frame = Frame.new(type: :settings, flags: [], stream_id: 0, payload: "\x00\x01\x00\x00\x10\x10\x00\x02\x00\x00\x00\x00\x00\x01\x00\x00\x10\x11")
|
42
|
+
ret = frame.parse_settings
|
43
|
+
assert_equal(0x1011, ret[:header_table_size])
|
44
|
+
assert_equal(0x0000, ret[:enable_push])
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
using Plum::BinaryString
|
4
|
+
|
5
|
+
class HTTPSConnectionNegotiationTest < Minitest::Test
|
6
|
+
def test_server_must_raise_cprotocol_error_invalid_magic_short
|
7
|
+
con = HTTPSConnection.new(StringIO.new)
|
8
|
+
assert_connection_error(:protocol_error) {
|
9
|
+
con << "HELLO"
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_server_must_raise_cprotocol_error_invalid_magic_long
|
14
|
+
con = HTTPSConnection.new(StringIO.new)
|
15
|
+
assert_connection_error(:protocol_error) {
|
16
|
+
con << ("HELLO" * 100) # over 24
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_server_must_raise_cprotocol_error_non_settings_after_magic
|
21
|
+
con = HTTPSConnection.new(StringIO.new)
|
22
|
+
con << Connection::CLIENT_CONNECTION_PREFACE
|
23
|
+
assert_connection_error(:protocol_error) {
|
24
|
+
con << Frame.new(type: :window_update, stream_id: 0, payload: "".push_uint32(1)).assemble
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_server_accept_fragmented_magic
|
29
|
+
magic = Connection::CLIENT_CONNECTION_PREFACE
|
30
|
+
con = HTTPSConnection.new(StringIO.new)
|
31
|
+
assert_no_error {
|
32
|
+
con << magic[0...5]
|
33
|
+
con << magic[5..-1]
|
34
|
+
con << Frame.new(type: :settings, stream_id: 0).assemble
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
using Plum::BinaryString
|
4
|
+
|
5
|
+
class StreamTest < Minitest::Test
|
6
|
+
def test_stream_illegal_frame_type
|
7
|
+
open_new_stream {|stream|
|
8
|
+
assert_connection_error(:protocol_error) {
|
9
|
+
stream.receive_frame(Frame.new(type: :goaway, stream_id: stream.id, payload: "\x00\x00\x00\x00"))
|
10
|
+
}
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_stream_unknown_frame_type
|
15
|
+
open_new_stream {|stream|
|
16
|
+
assert_no_error {
|
17
|
+
stream.receive_frame(Frame.new(type_value: 0x0f, stream_id: stream.id, payload: "\x00\x00\x00\x00"))
|
18
|
+
}
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_stream_close
|
23
|
+
open_new_stream(state: :half_closed_local) {|stream|
|
24
|
+
stream.close(:frame_size_error)
|
25
|
+
|
26
|
+
last = sent_frames.last
|
27
|
+
assert_equal(:rst_stream, last.type)
|
28
|
+
assert_equal(StreamError.new(:frame_size_error).http2_error_code, last.payload.uint32)
|
29
|
+
assert_equal(:closed, stream.state)
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|