@@ -29,7 +29,10 @@ groups() ->
2929 prop_duplicates_do_not_exhaust_limit ,
3030 prop_negative_content_length_rejected ,
3131 prop_valid_content_length_round_trips ,
32- prop_non_numeric_content_length_does_not_crash
32+ prop_non_numeric_content_length_does_not_crash ,
33+ prop_round_trip ,
34+ prop_incremental_parse ,
35+ prop_max_body_length_enforced
3336 ]}].
3437
3538% % Any frame with up to LIMIT unique header names parses successfully.
@@ -108,6 +111,84 @@ prop_non_numeric_content_length_does_not_crash(_Config) ->
108111 end )
109112 end , [], 1000 ).
110113
114+ % % Serialize then parse preserves command, headers, and body.
115+ % % Headers with escape-triggering characters (colon, backslash, LF, CR)
116+ % % exercise both the fast and slow parser paths.
117+ prop_round_trip (_Config ) ->
118+ run_proper (
119+ fun () ->
120+ ? FORALL ({RawPairs , Body },
121+ {resize (5 , list ({stomp_hdr_name (), stomp_hdr_value ()})),
122+ resize (200 , binary ())},
123+ begin
124+ Hdrs = maps :from_list (
125+ [{<<" destination" >>, <<" /queue/t" >>} |
126+ [{K , V } || {K , V } <- RawPairs ,
127+ K =/= <<" content-length" >>,
128+ K =/= <<" destination" >>]]),
129+ Frame = # stomp_frame {command = 'SEND' ,
130+ headers = Hdrs ,
131+ body_iolist_rev = Body },
132+ Bin = iolist_to_binary (rabbit_stomp_frame :serialize (Frame )),
133+ {ok , Parsed , _ } = parse (Bin ),
134+ Parsed # stomp_frame .command =:= 'SEND' andalso
135+ lists :all (
136+ fun ({K , V }) ->
137+ maps :get (K , Parsed # stomp_frame .headers , undefined ) =:= V
138+ end , maps :to_list (Hdrs )) andalso
139+ Body =:= body_to_binary (Parsed )
140+ end )
141+ end , [], 500 ).
142+
143+ % % Splitting a valid frame at any byte boundary and parsing in two
144+ % % chunks must produce the same frame as parsing in one call.
145+ prop_incremental_parse (_Config ) ->
146+ run_proper (
147+ fun () ->
148+ ? FORALL ({Body , N },
149+ {resize (200 , binary ()), non_neg_integer ()},
150+ begin
151+ Bin = iolist_to_binary (
152+ rabbit_stomp_frame :serialize (
153+ # stomp_frame {command = 'SEND' ,
154+ headers = #{<<" destination" >> => <<" /queue/t" >>},
155+ body_iolist_rev = Body })),
156+ Pos = N rem (byte_size (Bin ) + 1 ),
157+ <<C1 :Pos /binary , C2 /binary >> = Bin ,
158+ {ok , Full , _ } = parse (Bin ),
159+ Chunked = case parse (C1 ) of
160+ {ok , F , _ } -> F ;
161+ {more , St } ->
162+ {ok , F2 , _ } = rabbit_stomp_frame :parse (C2 , St ),
163+ F2
164+ end ,
165+ Full # stomp_frame .command =:= Chunked # stomp_frame .command andalso
166+ Full # stomp_frame .headers =:= Chunked # stomp_frame .headers andalso
167+ body_to_binary (Full ) =:= body_to_binary (Chunked )
168+ end )
169+ end , [], 1000 ).
170+
171+ % % A SEND frame exceeding max_body_length is always rejected.
172+ prop_max_body_length_enforced (_Config ) ->
173+ run_proper (
174+ fun () ->
175+ ? FORALL ({MaxLen , BodySize },
176+ {range (1 , 500 ), range (0 , 1000 )},
177+ begin
178+ Body = binary :copy (<<" x" >>, BodySize ),
179+ Bin = iolist_to_binary (
180+ rabbit_stomp_frame :serialize (
181+ # stomp_frame {command = 'SEND' ,
182+ headers = #{<<" destination" >> => <<" /queue/t" >>},
183+ body_iolist_rev = Body })),
184+ Config = # stomp_parser_config {max_body_length = MaxLen },
185+ case rabbit_stomp_frame :parse (Bin , rabbit_stomp_frame :initial_state (Config )) of
186+ {ok , _ , _ } -> BodySize =< MaxLen ;
187+ {error , _ } -> BodySize > MaxLen
188+ end
189+ end )
190+ end , [], 1000 ).
191+
111192% %-------------------------------------------------------------------
112193
113194unique_headers (N ) ->
@@ -122,13 +203,36 @@ send_frame(HdrName, HdrValue, Body) ->
122203 HdrName , " :" , HdrValue , " \n\n " ,
123204 Body , " \0 " ]).
124205
125- % % Generator for binaries that are not valid integer strings.
206+ % % Produces binaries that are not valid integer strings.
126207non_numeric_bin () ->
127208 ? SUCHTHAT (Bin ,
128209 ? LET (Chars , list (range (0 , 255 )),
129210 list_to_binary (
130211 [C || C <- Chars , C =/= $\n , C =/= $\r , C =/= $: , C =/= $\\ , C =/= 0 ])),
131212 not is_integer (catch binary_to_integer (string :trim (Bin )))).
132213
214+ % % Produces non-empty, no NUL header names. Biased towards escaped chars.
215+ stomp_hdr_name () ->
216+ ? SUCHTHAT (Bin ,
217+ ? LET (Chars , resize (15 , non_empty (list (stomp_char ()))),
218+ list_to_binary (Chars )),
219+ Bin =/= <<" content-length" >> andalso
220+ Bin =/= <<" destination" >>).
221+
222+ % % Produces no-NUL header values. Biased towards escaped chars.
223+ stomp_hdr_value () ->
224+ ? LET (Chars , resize (20 , list (stomp_char ())),
225+ list_to_binary (Chars )).
226+
227+ % % Biased towards characters that trigger the escape slow path.
228+ stomp_char () ->
229+ frequency ([{3 , $: }, {3 , $\\ }, {3 , $\n }, {3 , $\r },
230+ {88 , range (32 , 126 )}]).
231+
232+ body_to_binary (# stomp_frame {body_iolist_rev = Rev }) when is_list (Rev ) ->
233+ iolist_to_binary (lists :reverse (Rev ));
234+ body_to_binary (# stomp_frame {body_iolist_rev = Bin }) when is_binary (Bin ) ->
235+ Bin .
236+
133237parse (Bin ) ->
134238 rabbit_stomp_frame :parse (Bin , rabbit_stomp_frame :initial_state ()).
0 commit comments