stomp_parser 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module StompParser
2
+ VERSION = "1.0.0"
3
+ end
@@ -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