stomp_parser 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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