stomp_parser 1.0.0-universal-java
Sign up to get free protection for your applications and to get access to all the features.
- 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 +60 -0
- data/Rakefile +134 -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,155 @@
|
|
1
|
+
%%{
|
2
|
+
machine frame;
|
3
|
+
|
4
|
+
getkey (chunk.getbyte(p) ^ 128) - 128;
|
5
|
+
|
6
|
+
action mark {
|
7
|
+
mark = p
|
8
|
+
}
|
9
|
+
action mark_key {
|
10
|
+
mark_key = chunk.byteslice(mark, p - mark)
|
11
|
+
mark = nil
|
12
|
+
}
|
13
|
+
action mark_frame {
|
14
|
+
mark_frame = Frame.new(nil, nil)
|
15
|
+
mark_frame_size = 0
|
16
|
+
}
|
17
|
+
action check_frame_size {
|
18
|
+
mark_frame_size += 1
|
19
|
+
raise FrameSizeExceeded if mark_frame_size > max_frame_size
|
20
|
+
}
|
21
|
+
|
22
|
+
action write_command {
|
23
|
+
mark_frame.write_command(chunk.byteslice(mark, p - mark))
|
24
|
+
mark = nil
|
25
|
+
}
|
26
|
+
|
27
|
+
action write_header {
|
28
|
+
mark_frame.write_header(mark_key, chunk.byteslice(mark, p - mark))
|
29
|
+
mark_key = mark = nil
|
30
|
+
}
|
31
|
+
|
32
|
+
action write_body {
|
33
|
+
mark_frame.write_body(chunk.byteslice(mark, p - mark))
|
34
|
+
mark = nil
|
35
|
+
}
|
36
|
+
|
37
|
+
action finish_headers {
|
38
|
+
mark_content_length = mark_frame.content_length
|
39
|
+
}
|
40
|
+
|
41
|
+
action consume_null {
|
42
|
+
(p - mark) < mark_content_length if mark_content_length
|
43
|
+
}
|
44
|
+
|
45
|
+
action consume_octet {
|
46
|
+
if mark_content_length
|
47
|
+
(p - mark) < mark_content_length
|
48
|
+
else
|
49
|
+
true
|
50
|
+
end
|
51
|
+
}
|
52
|
+
|
53
|
+
action finish_frame {
|
54
|
+
yield mark_frame
|
55
|
+
mark_frame = nil
|
56
|
+
}
|
57
|
+
|
58
|
+
include frame_common "parser_common.rl";
|
59
|
+
}%%
|
60
|
+
|
61
|
+
module StompParser
|
62
|
+
class RubyParser
|
63
|
+
class State
|
64
|
+
def initialize
|
65
|
+
@cs = RubyParser.start
|
66
|
+
@chunk = nil
|
67
|
+
@mark = nil
|
68
|
+
@mark_key = nil
|
69
|
+
@mark_frame = nil
|
70
|
+
@mark_frame_size = nil
|
71
|
+
@mark_content_length = nil
|
72
|
+
end
|
73
|
+
|
74
|
+
# You want documentation? HAHA.
|
75
|
+
attr_accessor :chunk
|
76
|
+
attr_accessor :cs
|
77
|
+
attr_accessor :mark
|
78
|
+
attr_accessor :mark_key
|
79
|
+
attr_accessor :mark_frame
|
80
|
+
attr_accessor :mark_frame_size
|
81
|
+
attr_accessor :mark_content_length
|
82
|
+
end
|
83
|
+
|
84
|
+
# this manipulates the singleton class of our context,
|
85
|
+
# so we do not want to run this code very often or we
|
86
|
+
# bust our ruby method caching
|
87
|
+
%% write data noprefix;
|
88
|
+
|
89
|
+
# Parse a chunk of Stomp-formatted data into a Frame.
|
90
|
+
#
|
91
|
+
# @param [String] chunk
|
92
|
+
# @param [State] state previous parser state, or nil for initial state
|
93
|
+
# @param [Integer] max_frame_size
|
94
|
+
# @yield [frame] yields each frame as it is parsed
|
95
|
+
# @yieldparam frame [Frame]
|
96
|
+
def self._parse(chunk, state, max_frame_size)
|
97
|
+
chunk.force_encoding(Encoding::BINARY)
|
98
|
+
|
99
|
+
if state.chunk
|
100
|
+
p = state.chunk.bytesize
|
101
|
+
chunk = state.chunk << chunk
|
102
|
+
else
|
103
|
+
p = 0
|
104
|
+
end
|
105
|
+
|
106
|
+
pe = chunk.bytesize # special
|
107
|
+
|
108
|
+
cs = state.cs
|
109
|
+
mark = state.mark
|
110
|
+
mark_key = state.mark_key
|
111
|
+
mark_frame = state.mark_frame
|
112
|
+
mark_frame_size = state.mark_frame_size
|
113
|
+
mark_content_length = state.mark_content_length
|
114
|
+
|
115
|
+
%% write exec;
|
116
|
+
|
117
|
+
if mark
|
118
|
+
state.chunk = chunk
|
119
|
+
else
|
120
|
+
state.chunk = nil
|
121
|
+
end
|
122
|
+
|
123
|
+
state.cs = cs
|
124
|
+
state.mark = mark
|
125
|
+
state.mark_key = mark_key
|
126
|
+
state.mark_frame = mark_frame
|
127
|
+
state.mark_frame_size = mark_frame_size
|
128
|
+
state.mark_content_length = mark_content_length
|
129
|
+
|
130
|
+
if cs == RubyParser.error
|
131
|
+
StompParser.build_parse_error(chunk, p)
|
132
|
+
else
|
133
|
+
nil
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def initialize(max_frame_size = StompParser.max_frame_size)
|
138
|
+
@state = State.new
|
139
|
+
@max_frame_size = Integer(max_frame_size)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Parse a chunk.
|
143
|
+
#
|
144
|
+
# @param [String] chunk
|
145
|
+
# @yield [frame]
|
146
|
+
# @yieldparam [Frame] frame
|
147
|
+
def parse(chunk)
|
148
|
+
@error ||= self.class._parse(chunk, @state, @max_frame_size) do |frame|
|
149
|
+
yield frame
|
150
|
+
end
|
151
|
+
|
152
|
+
raise @error if @error
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
data/parser_common.rl
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
%%{
|
2
|
+
machine frame_common;
|
3
|
+
|
4
|
+
NULL = "\0";
|
5
|
+
EOL = "\r"? . "\n";
|
6
|
+
OCTET = any;
|
7
|
+
|
8
|
+
client_commands = "SEND" | "SUBSCRIBE" | "UNSUBSCRIBE" | "BEGIN" | "COMMIT" | "ABORT" | "ACK" | "NACK" | "DISCONNECT" | "CONNECT" | "STOMP";
|
9
|
+
server_commands = "CONNECTED" | "MESSAGE" | "RECEIPT" | "ERROR";
|
10
|
+
command = (client_commands | server_commands) > mark % write_command . EOL;
|
11
|
+
|
12
|
+
HEADER_ESCAPE = "\\" . ("\\" | "n" | "r" | "c");
|
13
|
+
HEADER_OCTET = HEADER_ESCAPE | (OCTET - "\r" - "\n" - "\\" - ":");
|
14
|
+
header_key = HEADER_OCTET+ > mark % mark_key;
|
15
|
+
header_value = HEADER_OCTET* > mark;
|
16
|
+
header = header_key . ":" . header_value;
|
17
|
+
headers = (header % write_header . EOL)* % finish_headers . EOL;
|
18
|
+
|
19
|
+
consume_body = (NULL when consume_null | ^NULL when consume_octet)*;
|
20
|
+
body = consume_body >from(mark) % write_body <: NULL;
|
21
|
+
|
22
|
+
frame = ((command > mark_frame) :> headers :> (body @ finish_frame)) $ check_frame_size;
|
23
|
+
|
24
|
+
stream := (EOL | frame)*;
|
25
|
+
}%%
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
require "stomp_parser"
|
3
|
+
require "benchmark/ips"
|
4
|
+
|
5
|
+
class Benchpress
|
6
|
+
attr_reader :options
|
7
|
+
|
8
|
+
def initialize(options, &body)
|
9
|
+
@options = options
|
10
|
+
instance_exec(self, &body)
|
11
|
+
end
|
12
|
+
|
13
|
+
def name
|
14
|
+
"#{options[:file]}:#{options[:line]} #{options[:desc]}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def setup(&block)
|
18
|
+
@setup = block
|
19
|
+
end
|
20
|
+
|
21
|
+
def code(&block)
|
22
|
+
@code = block
|
23
|
+
end
|
24
|
+
|
25
|
+
def assert(&block)
|
26
|
+
@assert = block
|
27
|
+
end
|
28
|
+
|
29
|
+
def run_initial
|
30
|
+
instance_exec(&@setup) if @setup
|
31
|
+
result = run
|
32
|
+
result = instance_exec(result, &@assert) if @assert
|
33
|
+
unless result
|
34
|
+
raise "#{name} code returns #{result.inspect}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def run
|
39
|
+
instance_exec(&@code)
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_proc
|
43
|
+
lambda { run }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def describe(description, &body)
|
48
|
+
file, line, _ = caller[0].split(':')
|
49
|
+
options = {
|
50
|
+
desc: description,
|
51
|
+
file: File.basename(file),
|
52
|
+
line: line,
|
53
|
+
}
|
54
|
+
|
55
|
+
$__benchmarks__ << Benchpress.new(options, &body)
|
56
|
+
end
|
57
|
+
|
58
|
+
$__benchmarks__ = []
|
59
|
+
|
60
|
+
at_exit do
|
61
|
+
reports = Benchmark.ips(time = 2) do |x|
|
62
|
+
$__benchmarks__.each do |bench|
|
63
|
+
5.times { bench.run_initial }
|
64
|
+
x.report(bench.name, &bench)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -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
|