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