summaryrefslogtreecommitdiff
path: root/deps/rabbitmq_stomp/src/rabbit_stomp_frame.erl
diff options
context:
space:
mode:
Diffstat (limited to 'deps/rabbitmq_stomp/src/rabbit_stomp_frame.erl')
-rw-r--r--deps/rabbitmq_stomp/src/rabbit_stomp_frame.erl266
1 files changed, 266 insertions, 0 deletions
diff --git a/deps/rabbitmq_stomp/src/rabbit_stomp_frame.erl b/deps/rabbitmq_stomp/src/rabbit_stomp_frame.erl
new file mode 100644
index 0000000000..6b91dc3748
--- /dev/null
+++ b/deps/rabbitmq_stomp/src/rabbit_stomp_frame.erl
@@ -0,0 +1,266 @@
+%% The contents of this file are subject to the Mozilla Public License
+%% Version 1.1 (the "License"); you may not use this file except in
+%% compliance with the License. You may obtain a copy of the License
+%% at https://www.mozilla.org/MPL/
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and
+%% limitations under the License.
+%%
+%% The Original Code is RabbitMQ.
+%%
+%% The Initial Developer of the Original Code is GoPivotal, Inc.
+%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved.
+%%
+
+%% stomp_frame implements the STOMP framing protocol "version 1.0", as
+%% per https://stomp.codehaus.org/Protocol
+
+-module(rabbit_stomp_frame).
+
+-include("rabbit_stomp_frame.hrl").
+-include("rabbit_stomp_headers.hrl").
+
+-export([parse/2, initial_state/0]).
+-export([header/2, header/3,
+ boolean_header/2, boolean_header/3,
+ integer_header/2, integer_header/3,
+ binary_header/2, binary_header/3]).
+-export([serialize/1, serialize/2]).
+
+initial_state() -> none.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% STOMP 1.1 frames basic syntax
+%% Rabbit modifications:
+%% o CR LF is equivalent to LF in all element terminators (eol).
+%% o Escape codes for header names and values include \r for CR
+%% and CR is not allowed.
+%% o Header names and values are not limited to UTF-8 strings.
+%% o Header values may contain unescaped colons
+%%
+%% frame_seq ::= *(noise frame)
+%% noise ::= *(NUL | eol)
+%% eol ::= LF | CR LF
+%% frame ::= cmd hdrs body NUL
+%% body ::= *OCTET
+%% cmd ::= 1*NOTEOL eol
+%% hdrs ::= *hdr eol
+%% hdr ::= hdrname COLON hdrvalue eol
+%% hdrname ::= 1*esc_char
+%% hdrvalue ::= *esc_char
+%% esc_char ::= HDROCT | BACKSLASH ESCCODE
+%%
+%% Terms in CAPS all represent sets (alternatives) of single octets.
+%% They are defined here using a small extension of BNF, minus (-):
+%%
+%% term1 - term2 denotes any of the possibilities in term1
+%% excluding those in term2.
+%% In this grammar minus is only used for sets of single octets.
+%%
+%% OCTET ::= '00'x..'FF'x % any octet
+%% NUL ::= '00'x % the zero octet
+%% LF ::= '\n' % '0a'x newline or linefeed
+%% CR ::= '\r' % '0d'x carriage return
+%% NOTEOL ::= OCTET - (CR | LF) % any octet except CR or LF
+%% BACKSLASH ::= '\\' % '5c'x
+%% ESCCODE ::= 'c' | 'n' | 'r' | BACKSLASH
+%% COLON ::= ':'
+%% HDROCT ::= NOTEOL - (COLON | BACKSLASH)
+%% % octets allowed in a header
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%% explicit frame characters
+-define(NUL, 0).
+-define(CR, $\r).
+-define(LF, $\n).
+-define(BSL, $\\).
+-define(COLON, $:).
+
+%% header escape codes
+-define(LF_ESC, $n).
+-define(BSL_ESC, $\\).
+-define(COLON_ESC, $c).
+-define(CR_ESC, $r).
+
+%% parser state
+-record(state, {acc, cmd, hdrs, hdrname}).
+
+parse(Content, {resume, Continuation}) -> Continuation(Content);
+parse(Content, none ) -> parser(Content, noframe, #state{}).
+
+more(Continuation) -> {more, {resume, Continuation}}.
+
+%% Single-function parser: Term :: noframe | command | headers | hdrname | hdrvalue
+%% general more and line-end detection
+parser(<<>>, Term , State) -> more(fun(Rest) -> parser(Rest, Term, State) end);
+parser(<<?CR>>, Term , State) -> more(fun(Rest) -> parser(<<?CR, Rest/binary>>, Term, State) end);
+parser(<<?CR, ?LF, Rest/binary>>, Term , State) -> parser(<<?LF, Rest/binary>>, Term, State);
+parser(<<?CR, Ch:8, _Rest/binary>>, Term , _State) -> {error, {unexpected_chars(Term), [?CR, Ch]}};
+%% escape processing (only in hdrname and hdrvalue terms)
+parser(<<?BSL>>, Term , State) -> more(fun(Rest) -> parser(<<?BSL, Rest/binary>>, Term, State) end);
+parser(<<?BSL, Ch:8, Rest/binary>>, Term , State)
+ when Term == hdrname;
+ Term == hdrvalue -> unescape(Ch, fun(Ech) -> parser(Rest, Term, accum(Ech, State)) end);
+%% inter-frame noise
+parser(<<?NUL, Rest/binary>>, noframe , State) -> parser(Rest, noframe, State);
+parser(<<?LF, Rest/binary>>, noframe , State) -> parser(Rest, noframe, State);
+%% detect transitions
+parser( Rest, noframe , State) -> goto(noframe, command, Rest, State);
+parser(<<?LF, Rest/binary>>, command , State) -> goto(command, headers, Rest, State);
+parser(<<?LF, Rest/binary>>, headers , State) -> goto(headers, body, Rest, State);
+parser( Rest, headers , State) -> goto(headers, hdrname, Rest, State);
+parser(<<?COLON, Rest/binary>>, hdrname , State) -> goto(hdrname, hdrvalue, Rest, State);
+parser(<<?LF, Rest/binary>>, hdrname , State) -> goto(hdrname, headers, Rest, State);
+parser(<<?LF, Rest/binary>>, hdrvalue, State) -> goto(hdrvalue, headers, Rest, State);
+%% accumulate
+parser(<<Ch:8, Rest/binary>>, Term , State) -> parser(Rest, Term, accum(Ch, State)).
+
+%% state transitions
+goto(noframe, command, Rest, State ) -> parser(Rest, command, State#state{acc = []});
+goto(command, headers, Rest, State = #state{acc = Acc} ) -> parser(Rest, headers, State#state{cmd = lists:reverse(Acc), hdrs = []});
+goto(headers, body, Rest, #state{cmd = Cmd, hdrs = Hdrs}) -> parse_body(Rest, #stomp_frame{command = Cmd, headers = Hdrs});
+goto(headers, hdrname, Rest, State ) -> parser(Rest, hdrname, State#state{acc = []});
+goto(hdrname, hdrvalue, Rest, State = #state{acc = Acc} ) -> parser(Rest, hdrvalue, State#state{acc = [], hdrname = lists:reverse(Acc)});
+goto(hdrname, headers, _Rest, #state{acc = Acc} ) -> {error, {header_no_value, lists:reverse(Acc)}}; % badly formed header -- fatal error
+goto(hdrvalue, headers, Rest, State = #state{acc = Acc, hdrs = Headers, hdrname = HdrName}) ->
+ parser(Rest, headers, State#state{hdrs = insert_header(Headers, HdrName, lists:reverse(Acc))}).
+
+%% error atom
+unexpected_chars(noframe) -> unexpected_chars_between_frames;
+unexpected_chars(command) -> unexpected_chars_in_command;
+unexpected_chars(hdrname) -> unexpected_chars_in_header;
+unexpected_chars(hdrvalue) -> unexpected_chars_in_header;
+unexpected_chars(_Term) -> unexpected_chars.
+
+%% general accumulation
+accum(Ch, State = #state{acc = Acc}) -> State#state{acc = [Ch | Acc]}.
+
+%% resolve escapes (with error processing)
+unescape(?LF_ESC, Fun) -> Fun(?LF);
+unescape(?BSL_ESC, Fun) -> Fun(?BSL);
+unescape(?COLON_ESC, Fun) -> Fun(?COLON);
+unescape(?CR_ESC, Fun) -> Fun(?CR);
+unescape(Ch, _Fun) -> {error, {bad_escape, [?BSL, Ch]}}.
+
+%% insert header unless aleady seen
+insert_header(Headers, Name, Value) ->
+ case lists:keymember(Name, 1, Headers) of
+ true -> Headers; % first header only
+ false -> [{Name, Value} | Headers]
+ end.
+
+parse_body(Content, Frame = #stomp_frame{command = Command}) ->
+ case Command of
+ "SEND" -> parse_body(Content, Frame, [], integer_header(Frame, ?HEADER_CONTENT_LENGTH, unknown));
+ _ -> parse_body(Content, Frame, [], unknown)
+ end.
+
+parse_body(Content, Frame, Chunks, unknown) ->
+ parse_body2(Content, Frame, Chunks, case firstnull(Content) of
+ -1 -> {more, unknown};
+ Pos -> {done, Pos}
+ end);
+parse_body(Content, Frame, Chunks, Remaining) ->
+ Size = byte_size(Content),
+ parse_body2(Content, Frame, Chunks, case Remaining >= Size of
+ true -> {more, Remaining - Size};
+ false -> {done, Remaining}
+ end).
+
+parse_body2(Content, Frame, Chunks, {more, Left}) ->
+ Chunks1 = finalize_chunk(Content, Chunks),
+ more(fun(Rest) -> parse_body(Rest, Frame, Chunks1, Left) end);
+parse_body2(Content, Frame, Chunks, {done, Pos}) ->
+ <<Chunk:Pos/binary, 0, Rest/binary>> = Content,
+ Body = lists:reverse(finalize_chunk(Chunk, Chunks)),
+ {ok, Frame#stomp_frame{body_iolist = Body}, Rest}.
+
+finalize_chunk(<<>>, Chunks) -> Chunks;
+finalize_chunk(Chunk, Chunks) -> [Chunk | Chunks].
+
+default_value({ok, Value}, _DefaultValue) -> Value;
+default_value(not_found, DefaultValue) -> DefaultValue.
+
+header(#stomp_frame{headers = Headers}, Key) ->
+ case lists:keysearch(Key, 1, Headers) of
+ {value, {_, Str}} -> {ok, Str};
+ _ -> not_found
+ end.
+
+header(F, K, D) -> default_value(header(F, K), D).
+
+boolean_header(#stomp_frame{headers = Headers}, Key) ->
+ case lists:keysearch(Key, 1, Headers) of
+ {value, {_, "true"}} -> {ok, true};
+ {value, {_, "false"}} -> {ok, false};
+ %% some Python clients serialize True/False as "True"/"False"
+ {value, {_, "True"}} -> {ok, true};
+ {value, {_, "False"}} -> {ok, false};
+ _ -> not_found
+ end.
+
+boolean_header(F, K, D) -> default_value(boolean_header(F, K), D).
+
+internal_integer_header(Headers, Key) ->
+ case lists:keysearch(Key, 1, Headers) of
+ {value, {_, Str}} -> {ok, list_to_integer(string:strip(Str))};
+ _ -> not_found
+ end.
+
+integer_header(#stomp_frame{headers = Headers}, Key) ->
+ internal_integer_header(Headers, Key).
+
+integer_header(F, K, D) -> default_value(integer_header(F, K), D).
+
+binary_header(F, K) ->
+ case header(F, K) of
+ {ok, Str} -> {ok, list_to_binary(Str)};
+ not_found -> not_found
+ end.
+
+binary_header(F, K, D) -> default_value(binary_header(F, K), D).
+
+serialize(Frame) ->
+ serialize(Frame, true).
+
+%% second argument controls whether a trailing linefeed
+%% character should be added, see rabbitmq/rabbitmq-stomp#39.
+serialize(Frame, true) ->
+ serialize(Frame, false) ++ [?LF];
+serialize(#stomp_frame{command = Command,
+ headers = Headers,
+ body_iolist = BodyFragments}, false) ->
+ Len = iolist_size(BodyFragments),
+ [Command, ?LF,
+ lists:map(fun serialize_header/1,
+ lists:keydelete(?HEADER_CONTENT_LENGTH, 1, Headers)),
+ if
+ Len > 0 -> [?HEADER_CONTENT_LENGTH ++ ":", integer_to_list(Len), ?LF];
+ true -> []
+ end,
+ ?LF, BodyFragments, 0].
+
+serialize_header({K, V}) when is_integer(V) -> hdr(escape(K), integer_to_list(V));
+serialize_header({K, V}) when is_boolean(V) -> hdr(escape(K), boolean_to_list(V));
+serialize_header({K, V}) when is_list(V) -> hdr(escape(K), escape(V)).
+
+boolean_to_list(true) -> "true";
+boolean_to_list(_) -> "false".
+
+hdr(K, V) -> [K, ?COLON, V, ?LF].
+
+escape(Str) -> [escape1(Ch) || Ch <- Str].
+
+escape1(?COLON) -> [?BSL, ?COLON_ESC];
+escape1(?BSL) -> [?BSL, ?BSL_ESC];
+escape1(?LF) -> [?BSL, ?LF_ESC];
+escape1(?CR) -> [?BSL, ?CR_ESC];
+escape1(Ch) -> Ch.
+
+firstnull(Content) -> firstnull(Content, 0).
+
+firstnull(<<>>, _N) -> -1;
+firstnull(<<0, _Rest/binary>>, N) -> N;
+firstnull(<<_Ch, Rest/binary>>, N) -> firstnull(Rest, N+1).