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,23 @@
|
|
1
|
+
package stomp_parser;
|
2
|
+
|
3
|
+
import org.jruby.Ruby;
|
4
|
+
import org.jruby.RubyClass;
|
5
|
+
import org.jruby.RubyModule;
|
6
|
+
import org.jruby.runtime.load.BasicLibraryService;
|
7
|
+
import org.jruby.runtime.builtin.IRubyObject;
|
8
|
+
import org.jruby.runtime.ObjectAllocator;
|
9
|
+
|
10
|
+
public class JavaParserService implements BasicLibraryService {
|
11
|
+
public boolean basicLoad(Ruby ruby) {
|
12
|
+
RubyModule mStomp = ruby.getClassFromPath("StompParser");
|
13
|
+
RubyClass cJavaParser = ruby.defineClassUnder("JavaParser", ruby.getObject(), JAVA_PARSER_ALLOCATOR, mStomp);
|
14
|
+
cJavaParser.defineAnnotatedMethods(JavaParser.class);
|
15
|
+
return true;
|
16
|
+
}
|
17
|
+
|
18
|
+
private static final ObjectAllocator JAVA_PARSER_ALLOCATOR = new ObjectAllocator() {
|
19
|
+
public IRubyObject allocate(Ruby ruby, RubyClass klass) {
|
20
|
+
return new JavaParser(ruby, klass);
|
21
|
+
}
|
22
|
+
};
|
23
|
+
}
|
@@ -0,0 +1,225 @@
|
|
1
|
+
#include <ruby.h>
|
2
|
+
|
3
|
+
#if DEBUG_H
|
4
|
+
# define DEBUG(fmt, ...) do { fprintf(stderr, fmt "\n", ##__VA_ARGS__); } while(0)
|
5
|
+
#else
|
6
|
+
# define DEBUG(...)
|
7
|
+
#endif
|
8
|
+
|
9
|
+
#define UNUSED(x) (void)(x)
|
10
|
+
#define MARK_LEN (p - mark)
|
11
|
+
#define MARK_STR_NEW() rb_str_new(mark, MARK_LEN)
|
12
|
+
|
13
|
+
#define true 1
|
14
|
+
#define false 0
|
15
|
+
|
16
|
+
typedef struct {
|
17
|
+
VALUE error;
|
18
|
+
long max_frame_size;
|
19
|
+
|
20
|
+
VALUE chunk;
|
21
|
+
const char *p;
|
22
|
+
int cs;
|
23
|
+
const char *mark;
|
24
|
+
VALUE mark_key;
|
25
|
+
VALUE mark_frame;
|
26
|
+
long mark_frame_size;
|
27
|
+
long mark_content_length;
|
28
|
+
} parser_state_t;
|
29
|
+
|
30
|
+
VALUE mStompParser = Qnil;
|
31
|
+
VALUE cFrame = Qnil;
|
32
|
+
VALUE eFrameSizeExceeded = Qnil;
|
33
|
+
ID g_new;
|
34
|
+
ID g_write_command;
|
35
|
+
ID g_write_header;
|
36
|
+
ID g_write_body;
|
37
|
+
ID g_content_length;
|
38
|
+
ID g_build_parse_error;
|
39
|
+
ID g_max_frame_size;
|
40
|
+
|
41
|
+
%%{
|
42
|
+
machine frame;
|
43
|
+
|
44
|
+
action mark {
|
45
|
+
mark = p;
|
46
|
+
}
|
47
|
+
|
48
|
+
action mark_frame {
|
49
|
+
mark_frame = rb_funcall(cFrame, g_new, 2, Qnil, Qnil);
|
50
|
+
mark_frame_size = 0;
|
51
|
+
}
|
52
|
+
|
53
|
+
action write_command {
|
54
|
+
rb_funcall(mark_frame, g_write_command, 1, MARK_STR_NEW());
|
55
|
+
mark = NULL;
|
56
|
+
}
|
57
|
+
|
58
|
+
action mark_key {
|
59
|
+
mark_key = MARK_STR_NEW();
|
60
|
+
mark = NULL;
|
61
|
+
}
|
62
|
+
|
63
|
+
action write_header {
|
64
|
+
rb_funcall(mark_frame, g_write_header, 2, mark_key, MARK_STR_NEW());
|
65
|
+
mark_key = Qnil;
|
66
|
+
mark = NULL;
|
67
|
+
}
|
68
|
+
|
69
|
+
action finish_headers {
|
70
|
+
VALUE length = rb_funcall(mark_frame, g_content_length, 0);
|
71
|
+
if ( ! NIL_P(length)) {
|
72
|
+
mark_content_length = NUM2LONG(length);
|
73
|
+
} else {
|
74
|
+
mark_content_length = -1;
|
75
|
+
}
|
76
|
+
}
|
77
|
+
|
78
|
+
action write_body {
|
79
|
+
rb_funcall(mark_frame, g_write_body, 1, MARK_STR_NEW());
|
80
|
+
mark = NULL;
|
81
|
+
}
|
82
|
+
|
83
|
+
action consume_null {
|
84
|
+
((mark_content_length != -1) && (MARK_LEN < mark_content_length))
|
85
|
+
}
|
86
|
+
|
87
|
+
action consume_octet {
|
88
|
+
((mark_content_length == -1) || (MARK_LEN < mark_content_length))
|
89
|
+
}
|
90
|
+
|
91
|
+
action check_frame_size {
|
92
|
+
mark_frame_size += 1;
|
93
|
+
if (mark_frame_size > max_frame_size) {
|
94
|
+
rb_raise(eFrameSizeExceeded, "");
|
95
|
+
}
|
96
|
+
}
|
97
|
+
|
98
|
+
action finish_frame {
|
99
|
+
rb_yield(mark_frame);
|
100
|
+
mark_frame = Qnil;
|
101
|
+
}
|
102
|
+
|
103
|
+
include frame_common "parser_common.rl";
|
104
|
+
|
105
|
+
write data noprefix;
|
106
|
+
}%%
|
107
|
+
|
108
|
+
static void parser_free(parser_state_t *state) {
|
109
|
+
// TODO: free memory inside struct!
|
110
|
+
xfree(state);
|
111
|
+
}
|
112
|
+
|
113
|
+
static void parser_mark(parser_state_t *state) {
|
114
|
+
rb_gc_mark(state->error);
|
115
|
+
rb_gc_mark(state->mark_key);
|
116
|
+
rb_gc_mark(state->mark_frame);
|
117
|
+
rb_gc_mark(state->chunk);
|
118
|
+
}
|
119
|
+
|
120
|
+
static VALUE parser_alloc(VALUE klass) {
|
121
|
+
parser_state_t *state = ALLOC(parser_state_t);
|
122
|
+
return Data_Wrap_Struct(klass, parser_mark, parser_free, state);
|
123
|
+
}
|
124
|
+
|
125
|
+
static VALUE parser_initialize(int argc, VALUE *argv, VALUE self) {
|
126
|
+
parser_state_t *state;
|
127
|
+
Data_Get_Struct(self, parser_state_t, state);
|
128
|
+
|
129
|
+
VALUE max_frame_size;
|
130
|
+
rb_scan_args(argc, argv, "01", &max_frame_size);
|
131
|
+
|
132
|
+
if (max_frame_size == Qnil) {
|
133
|
+
max_frame_size = rb_funcall(mStompParser, g_max_frame_size, 0);
|
134
|
+
}
|
135
|
+
|
136
|
+
state->error = Qnil;
|
137
|
+
state->max_frame_size = FIX2LONG(max_frame_size);
|
138
|
+
state->chunk = Qnil;
|
139
|
+
state->cs = start;
|
140
|
+
state->mark = NULL;
|
141
|
+
state->mark_key = Qnil;
|
142
|
+
state->mark_frame = Qnil;
|
143
|
+
state->mark_frame_size = 0;
|
144
|
+
state->mark_content_length = 0;
|
145
|
+
|
146
|
+
return self;
|
147
|
+
}
|
148
|
+
|
149
|
+
static VALUE parser_parse(VALUE self, VALUE new_chunk) {
|
150
|
+
parser_state_t *state;
|
151
|
+
Data_Get_Struct(self, parser_state_t, state);
|
152
|
+
|
153
|
+
if (NIL_P(state->error)) {
|
154
|
+
VALUE chunk = Qnil;
|
155
|
+
const char *p = NULL;
|
156
|
+
const char *mark = NULL;
|
157
|
+
|
158
|
+
if ( ! NIL_P(state->chunk)) {
|
159
|
+
long offset = RSTRING_LEN(state->chunk);
|
160
|
+
long mark_offset = state->mark - RSTRING_PTR(state->chunk);
|
161
|
+
|
162
|
+
chunk = rb_str_append(state->chunk, new_chunk);
|
163
|
+
p = RSTRING_PTR(chunk) + offset;
|
164
|
+
mark = RSTRING_PTR(chunk) + mark_offset;
|
165
|
+
} else {
|
166
|
+
chunk = new_chunk;
|
167
|
+
p = RSTRING_PTR(chunk);
|
168
|
+
}
|
169
|
+
|
170
|
+
const char *pe = RSTRING_END(chunk);
|
171
|
+
long max_frame_size = state->max_frame_size;
|
172
|
+
|
173
|
+
int cs = state->cs;
|
174
|
+
VALUE mark_key = state->mark_key;
|
175
|
+
VALUE mark_frame = state->mark_frame;
|
176
|
+
long mark_frame_size = state->mark_frame_size;
|
177
|
+
long mark_content_length = state->mark_content_length;
|
178
|
+
|
179
|
+
%% write exec;
|
180
|
+
|
181
|
+
if (mark != NULL) {
|
182
|
+
state->chunk = chunk;
|
183
|
+
} else {
|
184
|
+
state->chunk = Qnil;
|
185
|
+
}
|
186
|
+
|
187
|
+
state->cs = cs;
|
188
|
+
state->mark = mark;
|
189
|
+
state->mark_key = mark_key;
|
190
|
+
state->mark_frame = mark_frame;
|
191
|
+
state->mark_frame_size = mark_frame_size;
|
192
|
+
state->mark_content_length = mark_content_length;
|
193
|
+
|
194
|
+
if (cs == error) {
|
195
|
+
long index = p - RSTRING_PTR(chunk);
|
196
|
+
state->error = rb_funcall(mStompParser, g_build_parse_error, 2, chunk, LONG2NUM(index));
|
197
|
+
}
|
198
|
+
}
|
199
|
+
|
200
|
+
if ( ! NIL_P(state->error)) {
|
201
|
+
rb_exc_raise(state->error);
|
202
|
+
}
|
203
|
+
|
204
|
+
return Qnil;
|
205
|
+
}
|
206
|
+
|
207
|
+
void Init_c_parser(void) {
|
208
|
+
mStompParser = rb_const_get(rb_cObject, rb_intern("StompParser"));
|
209
|
+
cFrame = rb_const_get(mStompParser, rb_intern("Frame"));
|
210
|
+
eFrameSizeExceeded = rb_const_get(mStompParser, rb_intern("FrameSizeExceeded"));
|
211
|
+
|
212
|
+
g_new = rb_intern("new");
|
213
|
+
g_write_command = rb_intern("write_command");
|
214
|
+
g_write_header = rb_intern("write_header");
|
215
|
+
g_write_body = rb_intern("write_body");
|
216
|
+
g_content_length = rb_intern("content_length");
|
217
|
+
g_build_parse_error = rb_intern("build_parse_error");
|
218
|
+
g_max_frame_size = rb_intern("max_frame_size");
|
219
|
+
|
220
|
+
VALUE cParser = rb_define_class_under(mStompParser, "CParser", rb_cObject);
|
221
|
+
rb_define_alloc_func(cParser, parser_alloc);
|
222
|
+
|
223
|
+
rb_define_method(cParser, "initialize", parser_initialize, -1);
|
224
|
+
rb_define_method(cParser, "parse", parser_parse, 1);
|
225
|
+
}
|
@@ -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
|