Skip to content

Commit 4c60dcd

Browse files
Adopt rabbit_re in more places
1 parent 8f23124 commit 4c60dcd

4 files changed

Lines changed: 244 additions & 6 deletions

File tree

deps/rabbitmq_jms_topic_exchange/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ PROJECT = rabbitmq_jms_topic_exchange
22
PROJECT_DESCRIPTION = RabbitMQ JMS topic selector exchange plugin
33

44
DEPS = rabbit_common rabbit khepri khepri_mnesia_migration
5-
TEST_DEPS = rabbitmq_ct_helpers rabbitmq_ct_client_helpers amqp_client
5+
TEST_DEPS = rabbitmq_ct_helpers rabbitmq_ct_client_helpers amqp_client proper
66
LOCAL_DEPS = mnesia
77

88
DEP_EARLY_PLUGINS = rabbit_common/mk/rabbitmq-early-plugin.mk

deps/rabbitmq_jms_topic_exchange/src/sjx_evaluator.erl

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,13 @@ do_bin_op('/' , L, R) when L == 0 andalso R == 0 -> nan;
9797
do_bin_op(_,_,_) -> error.
9898

9999
isLike(undefined, _Patt) -> undefined;
100-
isLike(L, {regex, MP}) -> patt_match(L, MP);
101-
isLike(L, {Patt, Esc}) -> patt_match(L, pattern_of(Patt, Esc)).
100+
isLike(L, {regex, MP}) -> full_regex_match(L, MP);
101+
isLike(L, {Patt, Esc}) -> full_regex_match(L, pattern_of(Patt, Esc)).
102102

103-
patt_match(L, MP) ->
103+
%% Bounds match effort, as `rabbit_amqp_filter_sql`'s `like/2` does.
104+
full_regex_match(L, MP) ->
104105
BS = byte_size(L),
105-
case re:run(L, MP, [{capture, first}]) of
106+
case rabbit_re:run(L, MP, [{capture, first}]) of
106107
{match, [{0, BS}]} -> true;
107108
_ -> false
108109
end.
@@ -163,8 +164,9 @@ escape($\\) -> "\\\\";
163164
escape(Ch) -> Ch.
164165

165166
compile_re(error) -> error;
167+
%% `rabbit_re` also rejects overly long patterns before compiling them.
166168
compile_re(MatchMany) ->
167-
case re:compile(MatchMany)
169+
case rabbit_re:compile(MatchMany)
168170
of {ok, Rx} -> Rx;
169171
_ -> error
170172
end.
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
%% This Source Code Form is subject to the terms of the Mozilla Public
2+
%% License, v. 2.0. If a copy of the MPL was not distributed with this
3+
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
%%
5+
%% Copyright (c) 2007-2026 Broadcom. All Rights Reserved. The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
6+
%%
7+
8+
-module(rjms_topic_selector_like_SUITE).
9+
10+
-compile(export_all).
11+
12+
-include_lib("eunit/include/eunit.hrl").
13+
-include_lib("amqp_client/include/amqp_client.hrl").
14+
-include("rabbit_jms_topic_exchange.hrl").
15+
16+
-import(rabbit_ct_client_helpers, [open_connection_and_channel/1,
17+
close_connection_and_channel/2]).
18+
19+
-define(BSELECTARG(BinStr), {?RJMS_COMPILED_SELECTOR_ARG, longstr, BinStr}).
20+
-define(BASICMSG(Payload, Hdrs), #'amqp_msg'{props=#'P_basic'{headers=Hdrs}, payload=Payload}).
21+
22+
-define(CONFIRM_TIME_LIMIT_MS, 5000).
23+
24+
all() ->
25+
[
26+
{group, tests}
27+
].
28+
29+
groups() ->
30+
[
31+
{tests, [], [
32+
wildcard_heavy_selector_does_not_hang_routing
33+
]}
34+
].
35+
36+
%% -------------------------------------------------------------------
37+
%% Test suite setup/teardown.
38+
%% -------------------------------------------------------------------
39+
40+
init_per_suite(Config) ->
41+
rabbit_ct_helpers:log_environment(),
42+
rabbit_ct_helpers:run_setup_steps(Config).
43+
44+
end_per_suite(Config) ->
45+
rabbit_ct_helpers:run_teardown_steps(Config).
46+
47+
init_per_group(Group, Config) ->
48+
Config1 = rabbit_ct_helpers:set_config(Config, [
49+
{rmq_nodename_suffix, Group}
50+
]),
51+
rabbit_ct_helpers:run_setup_steps(Config1,
52+
rabbit_ct_broker_helpers:setup_steps() ++
53+
rabbit_ct_client_helpers:setup_steps()).
54+
55+
end_per_group(_, Config) ->
56+
rabbit_ct_helpers:run_teardown_steps(Config,
57+
rabbit_ct_client_helpers:teardown_steps() ++
58+
rabbit_ct_broker_helpers:teardown_steps()).
59+
60+
init_per_testcase(Testcase, Config) ->
61+
rabbit_ct_helpers:testcase_started(Config, Testcase).
62+
63+
end_per_testcase(Testcase, Config) ->
64+
rabbit_ct_helpers:testcase_finished(Config, Testcase).
65+
66+
%% -------------------------------------------------------------------
67+
%% Test cases.
68+
%% -------------------------------------------------------------------
69+
70+
wildcard_heavy_selector_does_not_hang_routing(Config) ->
71+
{Connection, Channel} = open_connection_and_channel(Config),
72+
#'confirm.select_ok'{} = amqp_channel:call(Channel, #'confirm.select'{}),
73+
74+
Exchange = declare_rjms_exchange(Channel, "wildcard_heavy_selector_exchange"),
75+
Q = declare_queue(Channel),
76+
Selector = compiled_selector({'like', {ident, <<"p">>}, wildcard_heavy_pattern(), no_escape}),
77+
bind_queue(Channel, Q, Exchange, <<"select-key">>, [?BSELECTARG(Selector)]),
78+
79+
Subject = binary:copy(<<"a">>, 3000),
80+
Publish = #'basic.publish'{exchange = Exchange, routing_key = <<"select-key">>},
81+
amqp_channel:cast(Channel, Publish, ?BASICMSG(<<"payload">>, [{<<"p">>, longstr, Subject}])),
82+
83+
{ElapsedUs, Confirmed} = timer:tc(fun() -> amqp_channel:wait_for_confirms(Channel, 5) end),
84+
?assertEqual(true, Confirmed),
85+
?assert(ElapsedUs < ?CONFIRM_TIME_LIMIT_MS * 1000),
86+
87+
#'basic.get_empty'{} = amqp_channel:call(Channel, #'basic.get'{queue = Q}),
88+
89+
amqp_channel:call(Channel, #'exchange.delete'{exchange = Exchange}),
90+
close_connection_and_channel(Connection, Channel),
91+
ok.
92+
93+
%% -------------------------------------------------------------------
94+
%% Helpers.
95+
%% -------------------------------------------------------------------
96+
97+
wildcard_heavy_pattern() ->
98+
iolist_to_binary([lists:duplicate(300, "%_"), "X"]).
99+
100+
%% Renders an already-parsed selector term back into the concrete syntax
101+
%% sjx_parser accepts, so a generated pattern doesn't need hand quoting.
102+
compiled_selector(Term) ->
103+
list_to_binary(io_lib:format("~p.", [Term])).
104+
105+
declare_rjms_exchange(Ch, XNameStr) ->
106+
Exchange = list_to_binary(XNameStr),
107+
Decl = #'exchange.declare'{ exchange = Exchange
108+
, type = <<"x-jms-topic">>
109+
, durable = false
110+
, auto_delete = false
111+
, arguments = [] },
112+
#'exchange.declare_ok'{} = amqp_channel:call(Ch, Decl),
113+
Exchange.
114+
115+
bind_queue(Ch, Q, Ex, RKey, Args) ->
116+
Binding = #'queue.bind'{ queue = Q
117+
, exchange = Ex
118+
, routing_key = RKey
119+
, arguments = Args
120+
},
121+
#'queue.bind_ok'{} = amqp_channel:call(Ch, Binding),
122+
ok.
123+
124+
declare_queue(Ch) ->
125+
#'queue.declare_ok'{queue = Q} = amqp_channel:call(Ch, #'queue.declare'{durable = true}),
126+
Q.
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
%% This Source Code Form is subject to the terms of the Mozilla Public
2+
%% License, v. 2.0. If a copy of the MPL was not distributed with this
3+
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
%%
5+
%% Copyright (c) 2007-2026 Broadcom. All Rights Reserved. The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
6+
%%
7+
8+
%% Property-based tests for sjx_evaluator's LIKE handling
9+
10+
-module(sjx_evaluator_like_prop_SUITE).
11+
12+
-compile(export_all).
13+
14+
-include_lib("common_test/include/ct.hrl").
15+
-include_lib("proper/include/proper.hrl").
16+
17+
-define(NUM_TESTS, 100).
18+
-define(TIME_LIMIT_MS, 50).
19+
20+
all() ->
21+
[
22+
wildcard_heavy_patterns_always_bounded,
23+
literal_pattern_matching_is_exact,
24+
oversized_pattern_is_rejected,
25+
malformed_escape_never_crashes
26+
].
27+
28+
init_per_suite(Config) ->
29+
rabbit_ct_helpers:log_environment(),
30+
rabbit_ct_helpers:run_setup_steps(Config).
31+
32+
end_per_suite(Config) ->
33+
rabbit_ct_helpers:run_teardown_steps(Config).
34+
35+
init_per_testcase(Testcase, Config) ->
36+
rabbit_ct_helpers:testcase_started(Config, Testcase).
37+
38+
end_per_testcase(Testcase, Config) ->
39+
rabbit_ct_helpers:testcase_finished(Config, Testcase).
40+
41+
wildcard_heavy_patterns_always_bounded(Config) ->
42+
Property = fun() -> prop_wildcard_heavy_patterns_always_bounded(Config) end,
43+
rabbit_ct_proper_helpers:run_proper(Property, [], ?NUM_TESTS).
44+
45+
prop_wildcard_heavy_patterns_always_bounded(_Config) ->
46+
MaxRepeats = (rabbit_re:max_pattern_length() - 1) div 2,
47+
?FORALL(
48+
{Repeats, SubjectLength},
49+
{range(1, MaxRepeats), range(100, 5000)},
50+
begin
51+
Selector = iolist_to_binary([lists:duplicate(Repeats, "%_"), "X"]),
52+
Subject = binary:copy(<<"a">>, SubjectLength),
53+
Headers = [{<<"p">>, longstr, Subject}],
54+
{ElapsedUs, _} = timer:tc(
55+
fun() -> sjx_evaluator:evaluate({'like', {ident, <<"p">>}, Selector, no_escape}, Headers) end),
56+
ElapsedUs < ?TIME_LIMIT_MS * 1000
57+
end).
58+
59+
literal_pattern_matching_is_exact(Config) ->
60+
Property = fun() -> prop_literal_pattern_matching_is_exact(Config) end,
61+
rabbit_ct_proper_helpers:run_proper(Property, [], ?NUM_TESTS).
62+
63+
prop_literal_pattern_matching_is_exact(_Config) ->
64+
?FORALL(
65+
{Literal, Suffix},
66+
{non_empty(literal_binary()), non_empty(literal_binary())},
67+
begin
68+
Matches = fun(Value) ->
69+
sjx_evaluator:evaluate({'like', {ident, <<"v">>}, Literal, no_escape},
70+
[{<<"v">>, longstr, Value}])
71+
end,
72+
Matches(Literal) andalso not Matches(<<Literal/binary, Suffix/binary>>)
73+
end).
74+
75+
oversized_pattern_is_rejected(Config) ->
76+
Property = fun() -> prop_oversized_pattern_is_rejected(Config) end,
77+
rabbit_ct_proper_helpers:run_proper(Property, [], ?NUM_TESTS).
78+
79+
prop_oversized_pattern_is_rejected(_Config) ->
80+
?FORALL(
81+
ExtraLength,
82+
range(1, 1000),
83+
begin
84+
Selector = binary:copy(<<"%">>, rabbit_re:max_pattern_length() + ExtraLength),
85+
Headers = [{<<"p">>, longstr, <<"anything">>}],
86+
not sjx_evaluator:evaluate({'like', {ident, <<"p">>}, Selector, no_escape}, Headers)
87+
end).
88+
89+
%% An escape argument that isn't a single byte is a shape the parser's
90+
%% grammar allows but sjx_evaluator can't turn into a regex; evaluation
91+
%% must return a boolean rather than raise.
92+
malformed_escape_never_crashes(Config) ->
93+
Property = fun() -> prop_malformed_escape_never_crashes(Config) end,
94+
rabbit_ct_proper_helpers:run_proper(Property, [], ?NUM_TESTS).
95+
96+
prop_malformed_escape_never_crashes(_Config) ->
97+
?FORALL(
98+
Escape,
99+
oneof([binary(), no_escape, regex, true, false, in]),
100+
begin
101+
Headers = [{<<"colour">>, longstr, <<"blue">>}],
102+
is_boolean(sjx_evaluator:evaluate({'like', {ident, <<"colour">>}, <<"bl%">>, Escape}, Headers))
103+
end).
104+
105+
%% Excludes % and _, which would change LIKE semantics rather than stay literal.
106+
literal_binary() ->
107+
?LET(Chars,
108+
list(oneof([range($a, $z), range($A, $Z), range($0, $9),
109+
$., $*, $+, $?, $^, $=, $!, $:, $$, ${, $}, $(, $), $|, $[, $], $/, $\\])),
110+
list_to_binary(Chars)).

0 commit comments

Comments
 (0)