stomp_parser 1.0.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 +7 -0
- data/.gitignore +33 -0
- data/.rspec +1 -0
- data/.travis.yml +7 -0
- data/Brewfile +2 -0
- data/Gemfile +11 -0
- data/MIT-LICENSE.txt +22 -0
- data/README.md +59 -0
- data/Rakefile +85 -0
- data/ext/java/stomp_parser/JavaParser.java.rl +179 -0
- data/ext/java/stomp_parser/JavaParserService.java +23 -0
- data/ext/stomp_parser/c_parser.c.rl +225 -0
- data/ext/stomp_parser/extconf.rb +15 -0
- data/lib/stomp_parser.rb +46 -0
- data/lib/stomp_parser/error.rb +18 -0
- data/lib/stomp_parser/frame.rb +133 -0
- data/lib/stomp_parser/ruby_parser.rb.rl +155 -0
- data/lib/stomp_parser/version.rb +3 -0
- data/parser_common.rl +25 -0
- data/spec/bench_helper.rb +67 -0
- data/spec/benchmarks/message_bench.rb +50 -0
- data/spec/benchmarks/parser_bench.rb +43 -0
- data/spec/profile.rb +27 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/stomp_parser/c_parser_spec.rb +5 -0
- data/spec/stomp_parser/java_parser_spec.rb +5 -0
- data/spec/stomp_parser/message_spec.rb +50 -0
- data/spec/stomp_parser/ruby_parser_spec.rb +3 -0
- data/spec/stomp_parser_spec.rb +9 -0
- data/spec/support/shared_parser_examples.rb +268 -0
- data/stomp_parser.gemspec +28 -0
- metadata +162 -0
@@ -0,0 +1,50 @@
|
|
1
|
+
require_relative "../bench_helper"
|
2
|
+
|
3
|
+
describe "Frame#to_str minimal" do |bench|
|
4
|
+
bench.setup do
|
5
|
+
@frame = StompParser::Frame.new("CONNECT", nil)
|
6
|
+
end
|
7
|
+
|
8
|
+
bench.code { @frame.to_str }
|
9
|
+
|
10
|
+
bench.assert do |frame_str|
|
11
|
+
frame_str == "CONNECT\ncontent-length:0\n\n\x00"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "Frame#to_str with header" do |bench|
|
16
|
+
bench.setup do
|
17
|
+
@frame = StompParser::Frame.new("CONNECT", { "heart-beat" => "0,0" }, nil)
|
18
|
+
end
|
19
|
+
|
20
|
+
bench.code { @frame.to_str }
|
21
|
+
|
22
|
+
bench.assert do |frame_str|
|
23
|
+
frame_str == "CONNECT\nheart-beat:0,0\ncontent-length:0\n\n\x00"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "Frame#to_str with headers and small body" do |bench|
|
28
|
+
bench.setup do
|
29
|
+
@frame = StompParser::Frame.new("CONNECT", { "some" => "header" }, "body")
|
30
|
+
end
|
31
|
+
|
32
|
+
bench.code { @frame.to_str }
|
33
|
+
|
34
|
+
bench.assert do |frame_str|
|
35
|
+
frame_str == "CONNECT\nsome:header\ncontent-length:4\n\nbody\x00"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "Frame#to_str with headers and large body" do |bench|
|
40
|
+
bench.setup do
|
41
|
+
large_binary = "b\x00" * 2 # make room for headers
|
42
|
+
@frame = StompParser::Frame.new("CONNECT", { "some" => "header" }, large_binary)
|
43
|
+
end
|
44
|
+
|
45
|
+
bench.code { @frame.to_str }
|
46
|
+
|
47
|
+
bench.assert do |frame_str|
|
48
|
+
frame_str == "CONNECT\nsome:header\ncontent-length:#{@frame.body.bytesize}\n\n#{@frame.body}\x00"
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require_relative "../bench_helper"
|
2
|
+
|
3
|
+
def parse_one(parser, data)
|
4
|
+
frame = nil
|
5
|
+
parser.parse(data) { |m| frame = m }
|
6
|
+
frame
|
7
|
+
end
|
8
|
+
|
9
|
+
%w[CParser JavaParser RubyParser].each do |parser|
|
10
|
+
parser = begin
|
11
|
+
StompParser.const_get(parser)
|
12
|
+
rescue NameError
|
13
|
+
next
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "#{parser}: minimal" do |bench|
|
17
|
+
bench.setup do
|
18
|
+
@parser = parser.new
|
19
|
+
@frame = "CONNECT\n\n\x00"
|
20
|
+
end
|
21
|
+
|
22
|
+
bench.code { parse_one(@parser, @frame) }
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "#{parser}: headers and small body" do |bench|
|
26
|
+
bench.setup do
|
27
|
+
@parser = parser.new
|
28
|
+
@frame = "CONNECT\ncontent-length:4\n\nbody\x00"
|
29
|
+
end
|
30
|
+
|
31
|
+
bench.code { parse_one(@parser, @frame) }
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "#{parser}: headers and large body" do |bench|
|
35
|
+
bench.setup do
|
36
|
+
@parser = parser.new
|
37
|
+
large_body = ("b" * (StompParser.max_frame_size - 50)) # make room for headers
|
38
|
+
@frame = "CONNECT\ncontent-length:#{large_body.bytesize}\n\n#{large_body}\x00"
|
39
|
+
end
|
40
|
+
|
41
|
+
bench.code { parse_one(@parser, @frame) }
|
42
|
+
end
|
43
|
+
end
|
data/spec/profile.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
require "stomp_parser"
|
3
|
+
require "perftools"
|
4
|
+
|
5
|
+
parser = StompParser::Parser.new
|
6
|
+
body_size = (1024 * 99) / 2
|
7
|
+
large_binary = "b\x00" * body_size # make room for headers
|
8
|
+
data = <<-MESSAGE
|
9
|
+
CONNECT
|
10
|
+
content-length:#{large_binary.bytesize}
|
11
|
+
|
12
|
+
#{large_binary}\x00
|
13
|
+
MESSAGE
|
14
|
+
stream = data + data + data + data
|
15
|
+
|
16
|
+
profile_output = File.expand_path("./profile/parser.profile", File.dirname(__FILE__))
|
17
|
+
PerfTools::CpuProfiler.start(profile_output) do
|
18
|
+
i = 100
|
19
|
+
loop do
|
20
|
+
parser.parse(data) do |frame|
|
21
|
+
# no op
|
22
|
+
end
|
23
|
+
|
24
|
+
i -= 1
|
25
|
+
break if i <= 0
|
26
|
+
end
|
27
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
describe StompParser::Frame do
|
2
|
+
describe "#content_length" do
|
3
|
+
it "returns content length if available" do
|
4
|
+
frame = StompParser::Frame.new("CONNECT", { "content-length" => "1337" }, nil)
|
5
|
+
frame.content_length.should eq 1337
|
6
|
+
end
|
7
|
+
|
8
|
+
it "returns nil if no content length defined" do
|
9
|
+
frame = StompParser::Frame.new("CONNECT", nil)
|
10
|
+
frame.content_length.should be_nil
|
11
|
+
end
|
12
|
+
|
13
|
+
it "raises an error if invalid content length defined" do
|
14
|
+
frame = StompParser::Frame.new("CONNECT", { "content-length" => "LAWL" }, nil)
|
15
|
+
expect { frame.content_length }.to raise_error(StompParser::Error, /invalid content length "LAWL"/)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "#to_str" do
|
20
|
+
specify "frame with command only" do
|
21
|
+
frame = StompParser::Frame.new("CONNECT", nil)
|
22
|
+
frame.to_str.should eq "CONNECT\ncontent-length:0\n\n\x00"
|
23
|
+
end
|
24
|
+
|
25
|
+
specify "frame with with headers" do
|
26
|
+
frame = StompParser::Frame.new("CONNECT", { "moo" => "cow", "boo" => "hoo" }, nil)
|
27
|
+
frame.to_str.should eq "CONNECT\nmoo:cow\nboo:hoo\ncontent-length:0\n\n\x00"
|
28
|
+
end
|
29
|
+
|
30
|
+
specify "frame with with body" do
|
31
|
+
frame = StompParser::Frame.new("CONNECT", "this is a body")
|
32
|
+
frame.to_str.should eq "CONNECT\ncontent-length:14\n\nthis is a body\x00"
|
33
|
+
end
|
34
|
+
|
35
|
+
specify "frame with escapeable characters in headers" do
|
36
|
+
frame = StompParser::Frame.new("CONNECT", { "k\\\n\r:" => "v\\\n\r:" }, nil)
|
37
|
+
frame.to_str.should eq "CONNECT\nk\\\\\\n\\r\\c:v\\\\\\n\\r\\c\ncontent-length:0\n\n\x00"
|
38
|
+
end
|
39
|
+
|
40
|
+
specify "frame with binary body" do
|
41
|
+
frame = StompParser::Frame.new("CONNECT", "\x00ab\x00")
|
42
|
+
frame.to_str.should eq "CONNECT\ncontent-length:4\n\n\x00ab\x00\x00"
|
43
|
+
end
|
44
|
+
|
45
|
+
specify "overrides user-specified content-length" do
|
46
|
+
frame = StompParser::Frame.new("CONNECT", { "content-length" => "10" }, "\x00ab\x00")
|
47
|
+
frame.to_str.should eq "CONNECT\ncontent-length:4\n\n\x00ab\x00\x00"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,268 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
RSpec.shared_examples_for "a stomp_parser parser" do
|
3
|
+
let(:parser) { described_class.new }
|
4
|
+
|
5
|
+
context "#parse" do
|
6
|
+
def parse_all(data)
|
7
|
+
frames = []
|
8
|
+
parser.parse(data) { |m| frames << m }
|
9
|
+
frames
|
10
|
+
end
|
11
|
+
|
12
|
+
it "parses frame as binary" do
|
13
|
+
frames = parse_all("CONNECT\n\n\x00")
|
14
|
+
frames.length.should eq(1)
|
15
|
+
frames[0].command.encoding.should eq Encoding::BINARY
|
16
|
+
end
|
17
|
+
|
18
|
+
context "command" do
|
19
|
+
it "can parse commands" do
|
20
|
+
frames = parse_all("CONNECT\n\n\x00")
|
21
|
+
frames.length.should eq(1)
|
22
|
+
frames[0].command.should eq("CONNECT")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context "headers" do
|
27
|
+
it "can parse simple headers" do
|
28
|
+
frames = parse_all("CONNECT\nmoo:cow\n\n\x00")
|
29
|
+
frames.length.should eq(1)
|
30
|
+
frames[0].headers.should eq("moo" => "cow")
|
31
|
+
end
|
32
|
+
|
33
|
+
it "can parse multiple headers" do
|
34
|
+
frames = parse_all("CONNECT\nmoo:cow\nbaah:sheep\n\n\x00")
|
35
|
+
frames.length.should eq(1)
|
36
|
+
frames[0].headers.should eq("moo" => "cow", "baah" => "sheep")
|
37
|
+
end
|
38
|
+
|
39
|
+
it "can parse headers with NULLs in them" do
|
40
|
+
frames = parse_all("CONNECT\nnull\x00:null\x00\n\n\x00")
|
41
|
+
frames.length.should eq(1)
|
42
|
+
frames[0].headers.should eq("null\x00" => "null\x00")
|
43
|
+
end
|
44
|
+
|
45
|
+
it "can parse headers with escape characters" do
|
46
|
+
frames = parse_all("CONNECT\nnull\\c:\\r\\n\\c\\\\\n\n\x00")
|
47
|
+
frames.length.should eq(1)
|
48
|
+
frames[0].headers.should eq("null:" => "\r\n:\\")
|
49
|
+
end
|
50
|
+
|
51
|
+
it "can parse headers with no value" do
|
52
|
+
frames = parse_all("CONNECT\nmoo:\n\n\x00")
|
53
|
+
frames.length.should eq(1)
|
54
|
+
frames[0].headers.should eq("moo" => nil)
|
55
|
+
end
|
56
|
+
|
57
|
+
it "nullifies previous headers" do
|
58
|
+
frames = parse_all("CONNECT\nmoo:\nmoo:hello\n\n\x00")
|
59
|
+
frames.length.should eq(1)
|
60
|
+
frames[0].headers.should eq("moo" => nil)
|
61
|
+
end
|
62
|
+
|
63
|
+
it "prioritises first header when given multiple of same key" do
|
64
|
+
frames = parse_all("CONNECT\nkey:first\nkey:second\n\n\x00")
|
65
|
+
frames.length.should eq(1)
|
66
|
+
frames[0].headers.should eq("key" => "first")
|
67
|
+
end
|
68
|
+
|
69
|
+
it "parses multibyte headers as UTF-8 even if content type specifies something else" do
|
70
|
+
frames = parse_all("MESSAGE\ncontent-type:text/plain;charset=ISO-8859-1\nwhät:üp\n\n\x00")
|
71
|
+
frames.length.should eq(1)
|
72
|
+
|
73
|
+
key, value = frames[0].headers.to_a[1]
|
74
|
+
|
75
|
+
key.should eq "wh\xC3\xA4t".force_encoding("UTF-8")
|
76
|
+
key.encoding.should eq Encoding::UTF_8
|
77
|
+
|
78
|
+
value.should eq "\xC3\xBCp".force_encoding("UTF-8")
|
79
|
+
value.encoding.should eq Encoding::UTF_8
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context "body" do
|
84
|
+
it "can parse body" do
|
85
|
+
frames = parse_all("CONNECT\n\nbody\x00")
|
86
|
+
frames.length.should eq(1)
|
87
|
+
frames[0].body.should eq "body"
|
88
|
+
end
|
89
|
+
|
90
|
+
it "can parse binary body" do
|
91
|
+
frames = parse_all("CONNECT\ncontent-length:5\n\nbo\x00dy\x00")
|
92
|
+
frames.length.should eq(1)
|
93
|
+
frames[0].body.should eq "bo\x00dy"
|
94
|
+
end
|
95
|
+
|
96
|
+
it "parses body as binary string when no content-type given" do
|
97
|
+
frames = parse_all("MESSAGE\n\nWhat üp\x00")
|
98
|
+
frames.length.should eq(1)
|
99
|
+
frames[0].body.should eq "What \xC3\xBCp".force_encoding("BINARY")
|
100
|
+
frames[0].body.encoding.should eq Encoding::BINARY
|
101
|
+
end
|
102
|
+
|
103
|
+
it "parses body as encoded string when content-type is a text type and charset is given" do
|
104
|
+
frames = parse_all("MESSAGE\ncontent-type:text/plain;charset=ISO-8859-1\n\nWhat \xFCp\x00")
|
105
|
+
frames.length.should eq(1)
|
106
|
+
frames[0].body.should eq "What \xFCp".force_encoding("iso-8859-1")
|
107
|
+
frames[0].body.encoding.should eq Encoding::ISO_8859_1
|
108
|
+
end
|
109
|
+
|
110
|
+
it "parses body as encoded string when content-type is not a text type and charset is given" do
|
111
|
+
frames = parse_all("MESSAGE\ncontent-type:application/octet-stream;charset=ISO-8859-1\n\nWhat \xFCp\x00")
|
112
|
+
frames.length.should eq(1)
|
113
|
+
frames[0].body.should eq "What \xFCp".force_encoding("iso-8859-1")
|
114
|
+
frames[0].body.encoding.should eq Encoding::ISO_8859_1
|
115
|
+
end
|
116
|
+
|
117
|
+
it "parses body as utf-8 encoded string when content-type is a text type and charset is not given" do
|
118
|
+
frames = parse_all("MESSAGE\ncontent-type:text/plain\n\nWhat üp\x00")
|
119
|
+
frames.length.should eq(1)
|
120
|
+
frames[0].body.should eq "What \xC3\xBCp".force_encoding("UTF-8")
|
121
|
+
frames[0].body.encoding.should eq Encoding::UTF_8
|
122
|
+
end
|
123
|
+
|
124
|
+
it "parses body as binary string when content-type is not a text type and charset is not given" do
|
125
|
+
frames = parse_all("MESSAGE\ncontent-type:application/octet-stream\n\nWhat \xFCp\x00")
|
126
|
+
frames.length.should eq(1)
|
127
|
+
frames[0].body.should eq "What \xFCp".force_encoding("BINARY")
|
128
|
+
frames[0].body.encoding.should eq Encoding::BINARY
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
context "multiple frames" do
|
133
|
+
it "yields multiple frames in a single invocation" do
|
134
|
+
frames = parse_all("CONNECT\n\n\x00CONNECT\n\n\x00CONNECT\n\n\x00")
|
135
|
+
frames.length.should eq(3)
|
136
|
+
frames.map(&:command).should eq %w[CONNECT CONNECT CONNECT]
|
137
|
+
frames.uniq.length.should eq(3)
|
138
|
+
end
|
139
|
+
|
140
|
+
it "allows newlines between frames" do
|
141
|
+
frames = parse_all("\n\r\n\nCONNECT\n\n\x00\n\n\r\nCONNECT\n\n\x00\n\n")
|
142
|
+
frames.length.should eq(2)
|
143
|
+
frames.map(&:command).should eq %w[CONNECT CONNECT]
|
144
|
+
frames.uniq.length.should eq(2)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
context "multiple invocations" do
|
149
|
+
it "parses simple split up frames" do
|
150
|
+
frames = parse_all("CONNECT\n")
|
151
|
+
frames.should be_empty
|
152
|
+
|
153
|
+
frames = parse_all("\n\x00")
|
154
|
+
frames.length.should eq(1)
|
155
|
+
frames[0].command.should eq "CONNECT"
|
156
|
+
end
|
157
|
+
|
158
|
+
it "parses frames split across buffer markings" do
|
159
|
+
frames = parse_all("\n\nCONN")
|
160
|
+
frames.should be_empty
|
161
|
+
|
162
|
+
frames = parse_all("ECT\n\n\x00")
|
163
|
+
frames.length.should eq(1)
|
164
|
+
frames[0].command.should eq "CONNECT"
|
165
|
+
end
|
166
|
+
|
167
|
+
it "parses frames split across header keys" do
|
168
|
+
frames = parse_all("CONNECT\nheader:")
|
169
|
+
frames.should be_empty
|
170
|
+
|
171
|
+
frames = parse_all("value\n\n\x00")
|
172
|
+
frames.length.should eq(1)
|
173
|
+
frames[0].command.should eq "CONNECT"
|
174
|
+
frames[0].headers.should eq("header" => "value")
|
175
|
+
end
|
176
|
+
|
177
|
+
it "parses binary frame split across body" do
|
178
|
+
frames = parse_all("CONNECT\ncontent-length:4\n\n\x00a")
|
179
|
+
frames.should be_empty
|
180
|
+
|
181
|
+
frames = parse_all("b\x00\x00")
|
182
|
+
frames.length.should eq(1)
|
183
|
+
frames[0].command.should eq "CONNECT"
|
184
|
+
frames[0].body.should eq("\x00ab\x00")
|
185
|
+
end
|
186
|
+
|
187
|
+
it "parses frames split across frames" do
|
188
|
+
frames = parse_all("CONNECT\n")
|
189
|
+
frames.should be_empty
|
190
|
+
|
191
|
+
frames = parse_all("\n\x00CONNEC")
|
192
|
+
frames.length.should eq(1)
|
193
|
+
frames[0].command.should eq "CONNECT"
|
194
|
+
|
195
|
+
frames = parse_all("T\n\n\x00")
|
196
|
+
frames.length.should eq(1)
|
197
|
+
frames[0].command.should eq "CONNECT"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
context "fails on invalid frames" do
|
202
|
+
specify "no block given" do
|
203
|
+
expect { parser.parse("CONNECT\n\n\x00") }.to raise_error(LocalJumpError)
|
204
|
+
end
|
205
|
+
|
206
|
+
specify "invalid command" do
|
207
|
+
expect { parser.parse("CONNET\n\n\x00") }.to raise_error(StompParser::ParseError)
|
208
|
+
end
|
209
|
+
|
210
|
+
specify "unfinished command" do
|
211
|
+
expect { parser.parse("CONNECT\x00") }.to raise_error(StompParser::ParseError)
|
212
|
+
end
|
213
|
+
|
214
|
+
specify "header with colon" do
|
215
|
+
expect { parser.parse("CONNECT\nfoo: :bar\n\n\x00") }.to raise_error(StompParser::ParseError)
|
216
|
+
end
|
217
|
+
|
218
|
+
specify "header with invalid escape" do
|
219
|
+
expect { parser.parse("CONNECT\nfoo:\\t\n\n\x00") }.to raise_error(StompParser::ParseError)
|
220
|
+
end
|
221
|
+
|
222
|
+
specify "body longer than content length" do
|
223
|
+
expect { parser.parse("CONNECT\ncontent-length:0\n\nx\x00") }.to raise_error(StompParser::ParseError)
|
224
|
+
end
|
225
|
+
|
226
|
+
specify "invalid content length" do
|
227
|
+
expect { parser.parse("CONNECT\ncontent-length:LAWL\n\nx\x00") }.to raise_error(StompParser::Error, /invalid content length "LAWL"/)
|
228
|
+
end
|
229
|
+
|
230
|
+
specify "re-trying invocation after an error" do
|
231
|
+
first_error = begin
|
232
|
+
parser.parse("CONNET")
|
233
|
+
rescue StompParser::ParseError => ex
|
234
|
+
ex
|
235
|
+
end
|
236
|
+
|
237
|
+
first_error.should be_a(StompParser::ParseError)
|
238
|
+
|
239
|
+
second_error = begin
|
240
|
+
parser.parse("")
|
241
|
+
rescue StompParser::ParseError => ex
|
242
|
+
ex
|
243
|
+
end
|
244
|
+
|
245
|
+
second_error.should eql(first_error)
|
246
|
+
end
|
247
|
+
|
248
|
+
specify "total size bigger than global max frame size setting" do
|
249
|
+
StompParser.stub(:max_frame_size => 30)
|
250
|
+
parser = described_class.new
|
251
|
+
parser.parse("CONNECT\n") # 8
|
252
|
+
parser.parse("header:value\n") # 21
|
253
|
+
expect {
|
254
|
+
parser.parse("other:val\n") # 31
|
255
|
+
}.to raise_error(StompParser::FrameSizeExceeded)
|
256
|
+
end
|
257
|
+
|
258
|
+
specify "total size bigger than local max frame size setting" do
|
259
|
+
parser = described_class.new(max_frame_size = 30)
|
260
|
+
parser.parse("CONNECT\n") # 8
|
261
|
+
parser.parse("header:value\n") # 21
|
262
|
+
expect {
|
263
|
+
parser.parse("other:val\n") # 31
|
264
|
+
}.to raise_error(StompParser::FrameSizeExceeded)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|