1%%% @doc multi-OTP version compatibility shim for working with URIs
2-module(rebar_uri).
3
4-export([
5         parse/1, parse/2, scheme_defaults/0,
6         append_path/2, percent_decode/1
7]).
8
9-type error() :: {error, atom(), term()}.
10
11-define(DEC2HEX(X),
12        if ((X) >= 0) andalso ((X) =< 9) -> (X) + $0;
13           ((X) >= 10) andalso ((X) =< 15) -> (X) + $A - 10
14        end).
15
16-define(HEX2DEC(X),
17        if ((X) >= $0) andalso ((X) =< $9) -> (X) - $0;
18           ((X) >= $A) andalso ((X) =< $F) -> (X) - $A + 10;
19           ((X) >= $a) andalso ((X) =< $f) -> (X) - $a + 10
20        end).
21
22-ifdef(OTP_RELEASE).
23-spec parse(URIString) -> URIMap when
24    URIString :: uri_string:uri_string(),
25    URIMap :: uri_string:uri_map() | uri_string:error().
26
27parse(URIString) ->
28    parse(URIString, []).
29
30parse(URIString, URIOpts) ->
31    case uri_string:parse(URIString) of
32        #{path := ""} = Map -> apply_opts(Map#{path => "/"}, URIOpts);
33        Map when is_map(Map) -> apply_opts(Map, URIOpts);
34        {error, _, _} = E -> E
35    end.
36-else.
37-spec parse(URIString) -> URIMap when
38    URIString :: iodata(),
39    URIMap :: map() | {error, term(), term()}.
40
41parse(URIString) ->
42    parse(URIString, []).
43
44parse(URIString, URIOpts) ->
45    case http_uri:parse(URIString, URIOpts) of
46        {error, Reason} ->
47            %% no additional parser/term info available to us,
48            %% e.g. see what uri_string returns in
49            %% uri_string:parse(<<"h$ttp:::://////lolz">>).
50            {error, "", Reason};
51        {ok, {Scheme, UserInfo, Host, Port, Path, Query}} ->
52            #{
53                scheme => rebar_utils:to_list(Scheme),
54                host => Host,
55                port => Port,
56                path => Path,
57                %% http_uri:parse/1 includes the leading question mark
58                %% in query string but uri_string:parse/1 leaves it out.
59                %% string:slice/2 isn't available in OTP <= 19.
60                query => case Query of
61                           [] -> "";
62                           _  -> string:substr(Query, 2)
63                         end,
64                userinfo => UserInfo
65            }
66    end.
67-endif.
68
69%% OTP 21+
70-ifdef(OTP_RELEASE).
71append_path(Url, ExtraPath) ->
72     case parse(Url) of
73         #{path := Path} = Map ->
74             FullPath = join(Path, ExtraPath),
75             {ok, uri_string:recompose(maps:update(path, FullPath, Map))};
76         _ ->
77             error
78     end.
79-else.
80append_path(Url, ExtraPath) ->
81     case parse(Url) of
82         #{scheme := Scheme, userinfo := UserInfo, host := Host,
83           port := Port, path := Path, query := Query} ->
84             ListScheme = rebar_utils:to_list(Scheme),
85             PrefixedQuery = case Query of
86                               []    -> [];
87                               Other -> lists:append(["?", Other])
88                             end,
89             NormPath = case Path of
90                            "" -> "/";
91                            _ -> Path
92                        end,
93             {ok, maybe_port(
94                Url, lists:append([ListScheme, "://", UserInfo, Host]),
95                [$: | rebar_utils:to_list(Port)],
96                lists:append([join(NormPath, ExtraPath), PrefixedQuery])
97             )};
98         _ ->
99             error
100     end.
101-endif.
102
103%% Taken from OTP 23.2
104-spec percent_decode(URI) -> Result when
105      URI :: uri_string:uri_string() | uri_string:uri_map(),
106      Result :: uri_string:uri_string() |
107                uri_string:uri_map() |
108                {error, {invalid, {atom(), {term(), term()}}}}.
109percent_decode(URIMap) when is_map(URIMap)->
110    Fun = fun (K,V) when K =:= userinfo; K =:= host; K =:= path;
111                         K =:= query; K =:= fragment ->
112                  case raw_decode(V) of
113                      {error, Reason, Input} ->
114                          throw({error, {invalid, {K, {Reason, Input}}}});
115                      Else ->
116                          Else
117                  end;
118              %% Handle port and scheme
119              (_,V) ->
120                  V
121          end,
122    try maps:map(Fun, URIMap)
123    catch throw:Return ->
124            Return
125    end;
126percent_decode(URI) when is_list(URI) orelse
127                         is_binary(URI) ->
128    raw_decode(URI).
129
130%% OTP 21+
131-ifdef(OTP_RELEASE).
132scheme_defaults() ->
133    %% no scheme defaults here; just custom ones
134    [].
135-else.
136scheme_defaults() ->
137    http_uri:scheme_defaults().
138-endif.
139
140join(URI, "") -> URI;
141join(URI, "/") -> URI;
142join("/", [$/|_] = Path) -> Path;
143join("/", Path) -> [$/ | Path];
144join("", [$/|_] = Path) -> Path;
145join("", Path) -> [$/ | Path];
146join([H|T], Path) -> [H | join(T, Path)].
147
148
149-ifdef(OTP_RELEASE).
150apply_opts(Map = #{port := _}, _) ->
151    Map;
152apply_opts(Map = #{scheme := Scheme}, URIOpts) ->
153    SchemeDefaults = proplists:get_value(scheme_defaults, URIOpts, []),
154    %% Here is the funky bit: don't add the port number if it's in a default
155    %% to maintain proper default behaviour.
156    try lists:keyfind(list_to_existing_atom(Scheme), 1, SchemeDefaults) of
157        {_, Port} ->
158            Map#{port => Port};
159        false ->
160            Map
161    catch
162        error:badarg -> % not an existing atom, not in the list
163            Map
164    end.
165-else.
166maybe_port(Url, Host, Port, PathQ) ->
167    case lists:prefix(Host ++ Port, Url) of
168        true -> Host ++ Port ++ PathQ; % port was explicit
169        false -> Host ++ PathQ % port was implicit
170    end.
171-endif.
172
173-spec raw_decode(list()|binary()) -> list() | binary() | error().
174raw_decode(Cs) ->
175    raw_decode(Cs, <<>>).
176
177raw_decode(L, Acc) when is_list(L) ->
178    try
179        B0 = unicode:characters_to_binary(L),
180        B1 = raw_decode(B0, Acc),
181        unicode:characters_to_list(B1)
182    catch
183        throw:{error, Atom, RestData} ->
184            {error, Atom, RestData}
185    end;
186raw_decode(<<$%,C0,C1,Cs/binary>>, Acc) ->
187    case is_hex_digit(C0) andalso is_hex_digit(C1) of
188        true ->
189            B = ?HEX2DEC(C0)*16+?HEX2DEC(C1),
190            raw_decode(Cs, <<Acc/binary, B>>);
191        false ->
192            throw({error,invalid_percent_encoding,<<$%,C0,C1>>})
193    end;
194raw_decode(<<C,Cs/binary>>, Acc) ->
195    raw_decode(Cs, <<Acc/binary, C>>);
196raw_decode(<<>>, Acc) ->
197    check_utf8(Acc).
198
199-spec is_hex_digit(char()) -> boolean().
200is_hex_digit(C)
201  when $0 =< C, C =< $9;$a =< C, C =< $f;$A =< C, C =< $F -> true;
202is_hex_digit(_) -> false.
203
204%% Returns Cs if it is utf8 encoded.
205check_utf8(Cs) ->
206    case unicode:characters_to_list(Cs) of
207        {incomplete,_,_} ->
208            throw({error,invalid_utf8,Cs});
209        {error,_,_} ->
210            throw({error,invalid_utf8,Cs});
211        _ -> Cs
212    end.
213