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-2021 VMware, Inc. or its affiliates.  All rights reserved.
6%%
7
8-module(wildcard).
9
10-export([match/2]).
11
12-spec match(Subject :: binary(), Pattern :: binary()) -> boolean().
13match(Subject, Pattern) ->
14    case parse_pattern(Pattern) of
15        [First | Rest] ->
16            FirstSize = byte_size(First),
17            case Subject of
18                % If a pattern does not start with a wildcard,
19                % do exact matching in the beginning of the subject
20                <<First:FirstSize/binary, _/binary>> ->
21                    scan(Subject, Rest, FirstSize, byte_size(Subject));
22                _ -> false
23            end;
24        invalid -> false
25    end.
26
27-spec scan(Subject :: binary(), Pattern :: [binary()],
28           Pos :: integer(), Length :: integer()) -> boolean().
29% Pattern ends with a wildcard
30scan(_Subject, [<<>>], _Pos, _Length) -> true;
31% Pattern is complete. Subject scan is complete
32scan(_Subject, [], Length, Length) -> true;
33% No more pattern but subject scan is not complete
34scan(_Subject, [], Pos, Length) when Pos =/= Length -> false;
35% Subject scan is complete but there are more pattern elements
36scan(_Subject, _NonEmpty, Length, Length) -> false;
37% Skip duplicate wildcards
38scan(Subject, [<<>> | Rest], Pos, Length) ->
39    scan(Subject, Rest, Pos, Length);
40% Every other Part is after a wildcard
41scan(Subject, [Part | Rest], Pos, Length) ->
42    PartSize = byte_size(Part),
43    case binary:match(Subject, Part, [{scope, {Pos, Length - Pos}}]) of
44        nomatch             -> false;
45        {PartPos, PartSize} ->
46            NewPos = PartPos + PartSize,
47            scan(Subject, Rest, NewPos, Length)
48    end.
49
50-spec parse_pattern(binary()) -> [binary()] | invalid.
51parse_pattern(Pattern) ->
52    Parts = binary:split(Pattern, <<"*">>, [global]),
53    try lists:map(fun(Part) -> cow_qs:urldecode(Part) end, Parts)
54    catch Type:Error ->
55        rabbit_log:warning("Invalid pattern ~p : ~p",
56                           [Pattern, {Type, Error}]),
57        invalid
58    end.
59