stomp_parser 1.0.0-universal-java

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,3 @@
1
+ module StompParser
2
+ VERSION = "1.0.0"
3
+ 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
@@ -0,0 +1,6 @@
1
+ require "bundler/setup"
2
+ require "stomp_parser"
3
+ require "pry"
4
+ require "timeout"
5
+
6
+ require "support/shared_parser_examples"
@@ -0,0 +1,5 @@
1
+ if defined?(StompParser::CParser)
2
+ describe StompParser::CParser do
3
+ it_behaves_like "a stomp_parser parser"
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ if defined?(StompParser::JavaParser)
2
+ describe StompParser::JavaParser do
3
+ it_behaves_like "a stomp_parser parser"
4
+ end
5
+ end
@@ -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