stomp_parser 1.0.0
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 +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,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "mkmf"
|
4
|
+
|
5
|
+
$CFLAGS << " -O3"
|
6
|
+
|
7
|
+
should_build = true
|
8
|
+
should_build &&= have_header "ruby.h"
|
9
|
+
should_build &&= defined?(RUBY_ENGINE) && %w[ruby rbx].include?(RUBY_ENGINE)
|
10
|
+
|
11
|
+
if should_build
|
12
|
+
create_makefile("stomp_parser/c_parser")
|
13
|
+
else
|
14
|
+
dummy_makefile(".")
|
15
|
+
end
|
data/lib/stomp_parser.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require "stomp_parser/version"
|
2
|
+
require "stomp_parser/error"
|
3
|
+
require "stomp_parser/frame"
|
4
|
+
require "stomp_parser/ruby_parser"
|
5
|
+
|
6
|
+
case RUBY_ENGINE
|
7
|
+
when "ruby", "rbx"
|
8
|
+
require "stomp_parser/c_parser"
|
9
|
+
when "jruby"
|
10
|
+
require "stomp_parser/java_parser"
|
11
|
+
end
|
12
|
+
|
13
|
+
module StompParser
|
14
|
+
Parser = if defined?(CParser)
|
15
|
+
CParser
|
16
|
+
elsif defined?(JavaParser)
|
17
|
+
JavaParser
|
18
|
+
else
|
19
|
+
RubyParser
|
20
|
+
end
|
21
|
+
|
22
|
+
@max_frame_size = 1024 * 10 # 10KB
|
23
|
+
|
24
|
+
class << self
|
25
|
+
attr_accessor :max_frame_size
|
26
|
+
|
27
|
+
# Create a parse error from a string chunk and an index.
|
28
|
+
#
|
29
|
+
# @api private
|
30
|
+
# @param [String] chunk
|
31
|
+
# @param [Integer] index
|
32
|
+
# @return [ParseError]
|
33
|
+
def build_parse_error(chunk, index)
|
34
|
+
ctx = 7
|
35
|
+
min = [0, index - ctx].max
|
36
|
+
len = ctx + 1 + ctx
|
37
|
+
context = chunk.byteslice(min, len).force_encoding("BINARY")
|
38
|
+
|
39
|
+
idx = index - min
|
40
|
+
chr = context[idx]
|
41
|
+
context[idx] = " -->#{chr}<-- "
|
42
|
+
|
43
|
+
ParseError.new("unexpected #{chr} in chunk (#{context.inspect})")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module StompParser
|
2
|
+
class Error < StandardError
|
3
|
+
end
|
4
|
+
|
5
|
+
# Errors raised by the Parser.
|
6
|
+
class ParseError < Error
|
7
|
+
end
|
8
|
+
|
9
|
+
# Raised when the Parser has reached the
|
10
|
+
# limit for how large a Frame may be.
|
11
|
+
#
|
12
|
+
# Protects against malicious clients trying to
|
13
|
+
# fill the available memory by sending very large
|
14
|
+
# frames, for example by sending an unlimited
|
15
|
+
# amount of headers.
|
16
|
+
class FrameSizeExceeded < ParseError
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module StompParser
|
2
|
+
class Frame
|
3
|
+
HEADER_TRANSLATIONS = {
|
4
|
+
'\\r' => "\r",
|
5
|
+
'\\n' => "\n",
|
6
|
+
'\\c' => ":",
|
7
|
+
'\\\\' => '\\',
|
8
|
+
}.freeze
|
9
|
+
HEADER_TRANSLATIONS_KEYS = Regexp.union(HEADER_TRANSLATIONS.keys).freeze
|
10
|
+
HEADER_REVERSE_TRANSLATIONS = HEADER_TRANSLATIONS.invert
|
11
|
+
HEADER_REVERSE_TRANSLATIONS_KEYS = Regexp.union(HEADER_REVERSE_TRANSLATIONS.keys).freeze
|
12
|
+
EMPTY = "".force_encoding("UTF-8").freeze
|
13
|
+
|
14
|
+
# @return [String]
|
15
|
+
attr_reader :command
|
16
|
+
|
17
|
+
# @return [Hash<String, String>]
|
18
|
+
attr_reader :headers
|
19
|
+
|
20
|
+
# @return [String]
|
21
|
+
attr_reader :body
|
22
|
+
|
23
|
+
# Construct a frame from a command, optional headers, and a body.
|
24
|
+
#
|
25
|
+
# @param [String] command
|
26
|
+
# @param [Hash<String, String>] headers
|
27
|
+
# @param [String] body
|
28
|
+
def initialize(command, headers = {}, body)
|
29
|
+
@command = command || EMPTY
|
30
|
+
@headers = headers
|
31
|
+
@body = body || EMPTY
|
32
|
+
end
|
33
|
+
|
34
|
+
# Content length of this frame, according to headers.
|
35
|
+
#
|
36
|
+
# @raise [ArgumentError] if content-length is not a valid integer
|
37
|
+
# @return [Integer, nil]
|
38
|
+
def content_length
|
39
|
+
if headers.has_key?("content-length")
|
40
|
+
begin
|
41
|
+
Integer(headers["content-length"])
|
42
|
+
rescue ArgumentError
|
43
|
+
raise Error, "invalid content length #{headers["content-length"].inspect}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def content_type
|
49
|
+
headers["content-type"]
|
50
|
+
end
|
51
|
+
|
52
|
+
# @raise [ArgumentError] if encoding does not exist
|
53
|
+
# @return [Encoding] body encoding, according to headers.
|
54
|
+
def content_encoding
|
55
|
+
if content_type
|
56
|
+
mime_type, charset = content_type.to_s.split(";")
|
57
|
+
mime_type = mime_type.to_s
|
58
|
+
charset = charset.to_s[/\Acharset=(.*)/, 1].to_s
|
59
|
+
|
60
|
+
if charset.empty? and mime_type.to_s.start_with?("text/")
|
61
|
+
Encoding::UTF_8
|
62
|
+
elsif charset.empty?
|
63
|
+
Encoding::BINARY
|
64
|
+
else
|
65
|
+
Encoding.find(charset)
|
66
|
+
end
|
67
|
+
else
|
68
|
+
Encoding::BINARY
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Change the command of this frame.
|
73
|
+
#
|
74
|
+
# @param [String] command
|
75
|
+
def write_command(command)
|
76
|
+
@command = command
|
77
|
+
end
|
78
|
+
|
79
|
+
# Write a single header to this frame.
|
80
|
+
#
|
81
|
+
# @param [String] key
|
82
|
+
# @param [String] value
|
83
|
+
def write_header(key, value)
|
84
|
+
# @see http://stomp.github.io/stomp-specification-1.2.html#Repeated_Header_Entries
|
85
|
+
key = translate_header(key)
|
86
|
+
@headers[key] = translate_header(value) unless @headers.has_key?(key)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Write the body to this frame.
|
90
|
+
#
|
91
|
+
# @param [String] body
|
92
|
+
def write_body(body)
|
93
|
+
@body = body.force_encoding(content_encoding)
|
94
|
+
end
|
95
|
+
|
96
|
+
# @return [String] a string-representation of this frame.
|
97
|
+
def to_str
|
98
|
+
frame = "".force_encoding("UTF-8")
|
99
|
+
frame << command << "\n"
|
100
|
+
|
101
|
+
outgoing_headers = headers.dup
|
102
|
+
outgoing_headers["content-length"] = body.bytesize
|
103
|
+
outgoing_headers.each do |key, value|
|
104
|
+
frame << serialize_header(key) << ":" << serialize_header(value) << "\n"
|
105
|
+
end
|
106
|
+
frame << "\n"
|
107
|
+
|
108
|
+
frame << body << "\x00"
|
109
|
+
frame
|
110
|
+
end
|
111
|
+
alias_method :to_s, :to_str
|
112
|
+
|
113
|
+
def [](key)
|
114
|
+
@headers[key]
|
115
|
+
end
|
116
|
+
|
117
|
+
def destination
|
118
|
+
self["destination"]
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
# @see http://stomp.github.io/stomp-specification-1.2.html#Value_Encoding
|
124
|
+
def translate_header(value)
|
125
|
+
value.gsub(HEADER_TRANSLATIONS_KEYS, HEADER_TRANSLATIONS).force_encoding(Encoding::UTF_8) unless value.empty?
|
126
|
+
end
|
127
|
+
|
128
|
+
# inverse of #translate_header
|
129
|
+
def serialize_header(value)
|
130
|
+
value.to_s.gsub(HEADER_REVERSE_TRANSLATIONS_KEYS, HEADER_REVERSE_TRANSLATIONS)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -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
|