%% @author Bob Ippolito <bob@mochimedia.com>
%% @copyright 2007 Mochi Media, Inc.

%% @doc Utilities for parsing and quoting.

-module(mochiweb_util).
-author('bob@mochimedia.com').
-export([join/2, quote_plus/1, urlencode/1, parse_qs/1, unquote/1]).
-export([path_split/1]).
-export([urlsplit/1, urlsplit_path/1, urlunsplit/1, urlunsplit_path/1]).
-export([guess_mime/1, parse_header/1]).
-export([shell_quote/1, cmd/1, cmd_string/1, cmd_port/2, cmd_status/1]).
-export([record_to_proplist/2, record_to_proplist/3]).
-export([safe_relative_path/1, partition/2]).
-export([parse_qvalues/1, pick_accepted_encodings/3]).
-export([make_io/1]).

-define(PERCENT, 37).  % $\%
-define(FULLSTOP, 46). % $\.
-define(IS_HEX(C), ((C >= $0 andalso C =< $9) orelse
                    (C >= $a andalso C =< $f) orelse
                    (C >= $A andalso C =< $F))).
-define(QS_SAFE(C), ((C >= $a andalso C =< $z) orelse
                     (C >= $A andalso C =< $Z) orelse
                     (C >= $0 andalso C =< $9) orelse
                     (C =:= ?FULLSTOP orelse C =:= $- orelse C =:= $~ orelse
                      C =:= $_))).

hexdigit(C) when C < 10 -> $0 + C;
hexdigit(C) when C < 16 -> $A + (C - 10).

unhexdigit(C) when C >= $0, C =< $9 -> C - $0;
unhexdigit(C) when C >= $a, C =< $f -> C - $a + 10;
unhexdigit(C) when C >= $A, C =< $F -> C - $A + 10.

%% @spec partition(String, Sep) -> {String, [], []} | {Prefix, Sep, Postfix}
%% @doc Inspired by Python 2.5's str.partition:
%%      partition("foo/bar", "/") = {"foo", "/", "bar"},
%%      partition("foo", "/") = {"foo", "", ""}.
partition(String, Sep) ->
    case partition(String, Sep, []) of
        undefined ->
            {String, "", ""};
        Result ->
            Result
    end.

partition("", _Sep, _Acc) ->
    undefined;
partition(S, Sep, Acc) ->
    case partition2(S, Sep) of
        undefined ->
            [C | Rest] = S,
            partition(Rest, Sep, [C | Acc]);
        Rest ->
            {lists:reverse(Acc), Sep, Rest}
    end.

partition2(Rest, "") ->
    Rest;
partition2([C | R1], [C | R2]) ->
    partition2(R1, R2);
partition2(_S, _Sep) ->
    undefined.



%% @spec safe_relative_path(string()) -> string() | undefined
%% @doc Return the reduced version of a relative path or undefined if it
%%      is not safe. safe relative paths can be joined with an absolute path
%%      and will result in a subdirectory of the absolute path.
safe_relative_path("/" ++ _) ->
    undefined;
safe_relative_path(P) ->
    safe_relative_path(P, []).

safe_relative_path("", Acc) ->
    case Acc of
        [] ->
            "";
        _ ->
            string:join(lists:reverse(Acc), "/")
    end;
safe_relative_path(P, Acc) ->
    case partition(P, "/") of
        {"", "/", _} ->
            %% /foo or foo//bar
            undefined;
        {"..", _, _} when Acc =:= [] ->
            undefined;
        {"..", _, Rest} ->
            safe_relative_path(Rest, tl(Acc));
        {Part, "/", ""} ->
            safe_relative_path("", ["", Part | Acc]);
        {Part, _, Rest} ->
            safe_relative_path(Rest, [Part | Acc])
    end.

%% @spec shell_quote(string()) -> string()
%% @doc Quote a string according to UNIX shell quoting rules, returns a string
%%      surrounded by double quotes.
shell_quote(L) ->
    shell_quote(L, [$\"]).

%% @spec cmd_port([string()], Options) -> port()
%% @doc open_port({spawn, mochiweb_util:cmd_string(Argv)}, Options).
cmd_port(Argv, Options) ->
    open_port({spawn, cmd_string(Argv)}, Options).

%% @spec cmd([string()]) -> string()
%% @doc os:cmd(cmd_string(Argv)).
cmd(Argv) ->
    os:cmd(cmd_string(Argv)).

%% @spec cmd_string([string()]) -> string()
%% @doc Create a shell quoted command string from a list of arguments.
cmd_string(Argv) ->
    string:join([shell_quote(X) || X <- Argv], " ").

%% @spec cmd_status([string()]) -> {ExitStatus::integer(), Stdout::binary()}
%% @doc Accumulate the output and exit status from the given application, will be
%%      spawned with cmd_port/2.
cmd_status(Argv) ->
    Port = cmd_port(Argv, [exit_status, stderr_to_stdout,
                           use_stdio, binary]),
    try cmd_loop(Port, [])
    after catch port_close(Port)
    end.

%% @spec cmd_loop(port(), list()) -> {ExitStatus::integer(), Stdout::binary()}
%% @doc Accumulate the output and exit status from a port.
cmd_loop(Port, Acc) ->
    receive
        {Port, {exit_status, Status}} ->
            {Status, iolist_to_binary(lists:reverse(Acc))};
        {Port, {data, Data}} ->
            cmd_loop(Port, [Data | Acc])
    end.

%% @spec join([iolist()], iolist()) -> iolist()
%% @doc Join a list of strings or binaries together with the given separator
%%      string or char or binary. The output is flattened, but may be an
%%      iolist() instead of a string() if any of the inputs are binary().
join([], _Separator) ->
    [];
join([S], _Separator) ->
    lists:flatten(S);
join(Strings, Separator) ->
    lists:flatten(revjoin(lists:reverse(Strings), Separator, [])).

revjoin([], _Separator, Acc) ->
    Acc;
revjoin([S | Rest], Separator, []) ->
    revjoin(Rest, Separator, [S]);
revjoin([S | Rest], Separator, Acc) ->
    revjoin(Rest, Separator, [S, Separator | Acc]).

%% @spec quote_plus(atom() | integer() | float() | string() | binary()) -> string()
%% @doc URL safe encoding of the given term.
quote_plus(Atom) when is_atom(Atom) ->
    quote_plus(atom_to_list(Atom));
quote_plus(Int) when is_integer(Int) ->
    quote_plus(integer_to_list(Int));
quote_plus(Binary) when is_binary(Binary) ->
    quote_plus(binary_to_list(Binary));
quote_plus(Float) when is_float(Float) ->
    quote_plus(mochinum:digits(Float));
quote_plus(String) ->
    quote_plus(String, []).

quote_plus([], Acc) ->
    lists:reverse(Acc);
quote_plus([C | Rest], Acc) when ?QS_SAFE(C) ->
    quote_plus(Rest, [C | Acc]);
quote_plus([$\s | Rest], Acc) ->
    quote_plus(Rest, [$+ | Acc]);
quote_plus([C | Rest], Acc) ->
    <<Hi:4, Lo:4>> = <<C>>,
    quote_plus(Rest, [hexdigit(Lo), hexdigit(Hi), ?PERCENT | Acc]).

%% @spec urlencode([{Key, Value}]) -> string()
%% @doc URL encode the property list.
urlencode(Props) ->
    Pairs = lists:foldr(
              fun ({K, V}, Acc) ->
                      [quote_plus(K) ++ "=" ++ quote_plus(V) | Acc]
              end, [], Props),
    string:join(Pairs, "&").

%% @spec parse_qs(string() | binary()) -> [{Key, Value}]
%% @doc Parse a query string or application/x-www-form-urlencoded.
parse_qs(Binary) when is_binary(Binary) ->
    parse_qs(binary_to_list(Binary));
parse_qs(String) ->
    parse_qs(String, []).

parse_qs([], Acc) ->
    lists:reverse(Acc);
parse_qs(String, Acc) ->
    {Key, Rest} = parse_qs_key(String),
    {Value, Rest1} = parse_qs_value(Rest),
    parse_qs(Rest1, [{Key, Value} | Acc]).

parse_qs_key(String) ->
    parse_qs_key(String, []).

parse_qs_key([], Acc) ->
    {qs_revdecode(Acc), ""};
parse_qs_key([$= | Rest], Acc) ->
    {qs_revdecode(Acc), Rest};
parse_qs_key(Rest=[$; | _], Acc) ->
    {qs_revdecode(Acc), Rest};
parse_qs_key(Rest=[$& | _], Acc) ->
    {qs_revdecode(Acc), Rest};
parse_qs_key([C | Rest], Acc) ->
    parse_qs_key(Rest, [C | Acc]).

parse_qs_value(String) ->
    parse_qs_value(String, []).

parse_qs_value([], Acc) ->
    {qs_revdecode(Acc), ""};
parse_qs_value([$; | Rest], Acc) ->
    {qs_revdecode(Acc), Rest};
parse_qs_value([$& | Rest], Acc) ->
    {qs_revdecode(Acc), Rest};
parse_qs_value([C | Rest], Acc) ->
    parse_qs_value(Rest, [C | Acc]).

%% @spec unquote(string() | binary()) -> string()
%% @doc Unquote a URL encoded string.
unquote(Binary) when is_binary(Binary) ->
    unquote(binary_to_list(Binary));
unquote(String) ->
    qs_revdecode(lists:reverse(String)).

qs_revdecode(S) ->
    qs_revdecode(S, []).

qs_revdecode([], Acc) ->
    Acc;
qs_revdecode([$+ | Rest], Acc) ->
    qs_revdecode(Rest, [$\s | Acc]);
qs_revdecode([Lo, Hi, ?PERCENT | Rest], Acc) when ?IS_HEX(Lo), ?IS_HEX(Hi) ->
    qs_revdecode(Rest, [(unhexdigit(Lo) bor (unhexdigit(Hi) bsl 4)) | Acc]);
qs_revdecode([C | Rest], Acc) ->
    qs_revdecode(Rest, [C | Acc]).

%% @spec urlsplit(Url) -> {Scheme, Netloc, Path, Query, Fragment}
%% @doc Return a 5-tuple, does not expand % escapes. Only supports HTTP style
%%      URLs.
urlsplit(Url) ->
    {Scheme, Url1} = urlsplit_scheme(Url),
    {Netloc, Url2} = urlsplit_netloc(Url1),
    {Path, Query, Fragment} = urlsplit_path(Url2),
    {Scheme, Netloc, Path, Query, Fragment}.

urlsplit_scheme(Url) ->
    case urlsplit_scheme(Url, []) of
        no_scheme ->
            {"", Url};
        Res ->
            Res
    end.

urlsplit_scheme([C | Rest], Acc) when ((C >= $a andalso C =< $z) orelse
                                       (C >= $A andalso C =< $Z) orelse
                                       (C >= $0 andalso C =< $9) orelse
                                       C =:= $+ orelse C =:= $- orelse
                                       C =:= $.) ->
    urlsplit_scheme(Rest, [C | Acc]);
urlsplit_scheme([$: | Rest], Acc=[_ | _]) ->
    {string:to_lower(lists:reverse(Acc)), Rest};
urlsplit_scheme(_Rest, _Acc) ->
    no_scheme.

urlsplit_netloc("//" ++ Rest) ->
    urlsplit_netloc(Rest, []);
urlsplit_netloc(Path) ->
    {"", Path}.

urlsplit_netloc("", Acc) ->
    {lists:reverse(Acc), ""};
urlsplit_netloc(Rest=[C | _], Acc) when C =:= $/; C =:= $?; C =:= $# ->
    {lists:reverse(Acc), Rest};
urlsplit_netloc([C | Rest], Acc) ->
    urlsplit_netloc(Rest, [C | Acc]).


%% @spec path_split(string()) -> {Part, Rest}
%% @doc Split a path starting from the left, as in URL traversal.
%%      path_split("foo/bar") = {"foo", "bar"},
%%      path_split("/foo/bar") = {"", "foo/bar"}.
path_split(S) ->
    path_split(S, []).

path_split("", Acc) ->
    {lists:reverse(Acc), ""};
path_split("/" ++ Rest, Acc) ->
    {lists:reverse(Acc), Rest};
path_split([C | Rest], Acc) ->
    path_split(Rest, [C | Acc]).


%% @spec urlunsplit({Scheme, Netloc, Path, Query, Fragment}) -> string()
%% @doc Assemble a URL from the 5-tuple. Path must be absolute.
urlunsplit({Scheme, Netloc, Path, Query, Fragment}) ->
    lists:flatten([case Scheme of "" -> "";  _ -> [Scheme, "://"] end,
                   Netloc,
                   urlunsplit_path({Path, Query, Fragment})]).

%% @spec urlunsplit_path({Path, Query, Fragment}) -> string()
%% @doc Assemble a URL path from the 3-tuple.
urlunsplit_path({Path, Query, Fragment}) ->
    lists:flatten([Path,
                   case Query of "" -> ""; _ -> [$? | Query] end,
                   case Fragment of "" -> ""; _ -> [$# | Fragment] end]).

%% @spec urlsplit_path(Url) -> {Path, Query, Fragment}
%% @doc Return a 3-tuple, does not expand % escapes. Only supports HTTP style
%%      paths.
urlsplit_path(Path) ->
    urlsplit_path(Path, []).

urlsplit_path("", Acc) ->
    {lists:reverse(Acc), "", ""};
urlsplit_path("?" ++ Rest, Acc) ->
    {Query, Fragment} = urlsplit_query(Rest),
    {lists:reverse(Acc), Query, Fragment};
urlsplit_path("#" ++ Rest, Acc) ->
    {lists:reverse(Acc), "", Rest};
urlsplit_path([C | Rest], Acc) ->
    urlsplit_path(Rest, [C | Acc]).

urlsplit_query(Query) ->
    urlsplit_query(Query, []).

urlsplit_query("", Acc) ->
    {lists:reverse(Acc), ""};
urlsplit_query("#" ++ Rest, Acc) ->
    {lists:reverse(Acc), Rest};
urlsplit_query([C | Rest], Acc) ->
    urlsplit_query(Rest, [C | Acc]).

%% @spec guess_mime(string()) -> string()
%% @doc  Guess the mime type of a file by the extension of its filename.
guess_mime(File) ->
    case mochiweb_mime:from_extension(filename:extension(File)) of
        undefined ->
            "text/plain";
        Mime ->
            Mime
    end.

%% @spec parse_header(string()) -> {Type, [{K, V}]}
%% @doc  Parse a Content-Type like header, return the main Content-Type
%%       and a property list of options.
parse_header(String) ->
    %% TODO: This is exactly as broken as Python's cgi module.
    %%       Should parse properly like mochiweb_cookies.
    [Type | Parts] = [string:strip(S) || S <- string:tokens(String, ";")],
    F = fun (S, Acc) ->
                case lists:splitwith(fun (C) -> C =/= $= end, S) of
                    {"", _} ->
                        %% Skip anything with no name
                        Acc;
                    {_, ""} ->
                        %% Skip anything with no value
                        Acc;
                    {Name, [$\= | Value]} ->
                        [{string:to_lower(string:strip(Name)),
                          unquote_header(string:strip(Value))} | Acc]
                end
        end,
    {string:to_lower(Type),
     lists:foldr(F, [], Parts)}.

unquote_header("\"" ++ Rest) ->
    unquote_header(Rest, []);
unquote_header(S) ->
    S.

unquote_header("", Acc) ->
    lists:reverse(Acc);
unquote_header("\"", Acc) ->
    lists:reverse(Acc);
unquote_header([$\\, C | Rest], Acc) ->
    unquote_header(Rest, [C | Acc]);
unquote_header([C | Rest], Acc) ->
    unquote_header(Rest, [C | Acc]).

%% @spec record_to_proplist(Record, Fields) -> proplist()
%% @doc calls record_to_proplist/3 with a default TypeKey of '__record'
record_to_proplist(Record, Fields) ->
    record_to_proplist(Record, Fields, '__record').

%% @spec record_to_proplist(Record, Fields, TypeKey) -> proplist()
%% @doc Return a proplist of the given Record with each field in the
%%      Fields list set as a key with the corresponding value in the Record.
%%      TypeKey is the key that is used to store the record type
%%      Fields should be obtained by calling record_info(fields, record_type)
%%      where record_type is the record type of Record
record_to_proplist(Record, Fields, TypeKey)
  when tuple_size(Record) - 1 =:= length(Fields) ->
    lists:zip([TypeKey | Fields], tuple_to_list(Record)).


shell_quote([], Acc) ->
    lists:reverse([$\" | Acc]);
shell_quote([C | Rest], Acc) when C =:= $\" orelse C =:= $\` orelse
                                  C =:= $\\ orelse C =:= $\$ ->
    shell_quote(Rest, [C, $\\ | Acc]);
shell_quote([C | Rest], Acc) ->
    shell_quote(Rest, [C | Acc]).

%% @spec parse_qvalues(string()) -> [qvalue()] | invalid_qvalue_string
%% @type qvalue() = {encoding(), float()}.
%% @type encoding() = string().
%%
%% @doc Parses a list (given as a string) of elements with Q values associated
%%      to them. Elements are separated by commas and each element is separated
%%      from its Q value by a semicolon. Q values are optional but when missing
%%      the value of an element is considered as 1.0. A Q value is always in the
%%      range [0.0, 1.0]. A Q value list is used for example as the value of the
%%      HTTP "Accept-Encoding" header.
%%
%%      Q values are described in section 2.9 of the RFC 2616 (HTTP 1.1).
%%
%%      Example:
%%
%%      parse_qvalues("gzip; q=0.5, deflate, identity;q=0.0") ->
%%          [{"gzip", 0.5}, {"deflate", 1.0}, {"identity", 0.0}]
%%
parse_qvalues(QValuesStr) ->
    try
        {ok, Re} = re:compile("^\\s*q\\s*=\\s*((?:0|1)(?:\\.\\d{1,3})?)\\s*$"),
        lists:map(
            fun(Pair) ->
                case string:tokens(Pair, ";") of
                    [Enc] ->
                        {string:strip(Enc), 1.0};
                    [Enc, QStr] ->
                        case re:run(QStr, Re, [{capture, [1], list}]) of
                            {match, [Q]} ->
                                QVal = case Q of
                                    "0" ->
                                        0.0;
                                    "1" ->
                                        1.0;
                                    Else ->
                                        list_to_float(Else)
                                end,
                                case QVal < 0.0 orelse QVal > 1.0 of
                                    false ->
                                        {string:strip(Enc), QVal}
                                end
                        end
                end
            end,
            string:tokens(string:to_lower(QValuesStr), ",")
        )
    catch
        _Type:_Error ->
            invalid_qvalue_string
    end.

%% @spec pick_accepted_encodings([qvalue()], [encoding()], encoding()) ->
%%    [encoding()]
%%
%% @doc Determines which encodings specified in the given Q values list are
%%      valid according to a list of supported encodings and a default encoding.
%%
%%      The returned list of encodings is sorted, descendingly, according to the
%%      Q values of the given list. The last element of this list is the given
%%      default encoding unless this encoding is explicitily or implicitily
%%      marked with a Q value of 0.0 in the given Q values list.
%%      Note: encodings with the same Q value are kept in the same order as
%%            found in the input Q values list.
%%
%%      This encoding picking process is described in section 14.3 of the
%%      RFC 2616 (HTTP 1.1).
%%
%%      Example:
%%
%%      pick_accepted_encodings(
%%          [{"gzip", 0.5}, {"deflate", 1.0}],
%%          ["gzip", "identity"],
%%          "identity"
%%      ) ->
%%          ["gzip", "identity"]
%%
pick_accepted_encodings(AcceptedEncs, SupportedEncs, DefaultEnc) ->
    SortedQList = lists:reverse(
        lists:sort(fun({_, Q1}, {_, Q2}) -> Q1 < Q2 end, AcceptedEncs)
    ),
    {Accepted, Refused} = lists:foldr(
        fun({E, Q}, {A, R}) ->
            case Q > 0.0 of
                true ->
                    {[E | A], R};
                false ->
                    {A, [E | R]}
            end
        end,
        {[], []},
        SortedQList
    ),
    Refused1 = lists:foldr(
        fun(Enc, Acc) ->
            case Enc of
                "*" ->
                    lists:subtract(SupportedEncs, Accepted) ++ Acc;
                _ ->
                    [Enc | Acc]
            end
        end,
        [],
        Refused
    ),
    Accepted1 = lists:foldr(
        fun(Enc, Acc) ->
            case Enc of
                "*" ->
                    lists:subtract(SupportedEncs, Accepted ++ Refused1) ++ Acc;
                _ ->
                    [Enc | Acc]
            end
        end,
        [],
        Accepted
    ),
    Accepted2 = case lists:member(DefaultEnc, Accepted1) of
        true ->
            Accepted1;
        false ->
            Accepted1 ++ [DefaultEnc]
    end,
    [E || E <- Accepted2, lists:member(E, SupportedEncs),
        not lists:member(E, Refused1)].

make_io(Atom) when is_atom(Atom) ->
    atom_to_list(Atom);
make_io(Integer) when is_integer(Integer) ->
    integer_to_list(Integer);
make_io(Io) when is_list(Io); is_binary(Io) ->
    Io.

%%
%% Tests
%%
-include_lib("eunit/include/eunit.hrl").
-ifdef(TEST).

make_io_test() ->
    ?assertEqual(
       <<"atom">>,
       iolist_to_binary(make_io(atom))),
    ?assertEqual(
       <<"20">>,
       iolist_to_binary(make_io(20))),
    ?assertEqual(
       <<"list">>,
       iolist_to_binary(make_io("list"))),
    ?assertEqual(
       <<"binary">>,
       iolist_to_binary(make_io(<<"binary">>))),
    ok.

-record(test_record, {field1=f1, field2=f2}).
record_to_proplist_test() ->
    ?assertEqual(
       [{'__record', test_record},
        {field1, f1},
        {field2, f2}],
       record_to_proplist(#test_record{}, record_info(fields, test_record))),
    ?assertEqual(
       [{'typekey', test_record},
        {field1, f1},
        {field2, f2}],
       record_to_proplist(#test_record{},
                          record_info(fields, test_record),
                          typekey)),
    ok.

shell_quote_test() ->
    ?assertEqual(
       "\"foo \\$bar\\\"\\`' baz\"",
       shell_quote("foo $bar\"`' baz")),
    ok.

cmd_port_test_spool(Port, Acc) ->
    receive
        {Port, eof} ->
            Acc;
        {Port, {data, {eol, Data}}} ->
            cmd_port_test_spool(Port, ["\n", Data | Acc]);
        {Port, Unknown} ->
            throw({unknown, Unknown})
    after 100 ->
            throw(timeout)
    end.

cmd_port_test() ->
    Port = cmd_port(["echo", "$bling$ `word`!"],
                    [eof, stream, {line, 4096}]),
    Res = try lists:append(lists:reverse(cmd_port_test_spool(Port, [])))
          after catch port_close(Port)
          end,
    self() ! {Port, wtf},
    try cmd_port_test_spool(Port, [])
    catch throw:{unknown, wtf} -> ok
    end,
    try cmd_port_test_spool(Port, [])
    catch throw:timeout -> ok
    end,
    ?assertEqual(
       "$bling$ `word`!\n",
       Res).

cmd_test() ->
    ?assertEqual(
       "$bling$ `word`!\n",
       cmd(["echo", "$bling$ `word`!"])),
    ok.

cmd_string_test() ->
    ?assertEqual(
       "\"echo\" \"\\$bling\\$ \\`word\\`!\"",
       cmd_string(["echo", "$bling$ `word`!"])),
    ok.

cmd_status_test() ->
    ?assertEqual(
       {0, <<"$bling$ `word`!\n">>},
       cmd_status(["echo", "$bling$ `word`!"])),
    ok.


parse_header_test() ->
    ?assertEqual(
       {"multipart/form-data", [{"boundary", "AaB03x"}]},
       parse_header("multipart/form-data; boundary=AaB03x")),
    %% This tests (currently) intentionally broken behavior
    ?assertEqual(
       {"multipart/form-data",
        [{"b", ""},
         {"cgi", "is"},
         {"broken", "true\"e"}]},
       parse_header("multipart/form-data;b=;cgi=\"i\\s;broken=true\"e;=z;z")),
    ok.

guess_mime_test() ->
    "text/plain" = guess_mime(""),
    "text/plain" = guess_mime(".text"),
    "application/zip" = guess_mime(".zip"),
    "application/zip" = guess_mime("x.zip"),
    "text/html" = guess_mime("x.html"),
    "application/xhtml+xml" = guess_mime("x.xhtml"),
    ok.

path_split_test() ->
    {"", "foo/bar"} = path_split("/foo/bar"),
    {"foo", "bar"} = path_split("foo/bar"),
    {"bar", ""} = path_split("bar"),
    ok.

urlsplit_test() ->
    {"", "", "/foo", "", "bar?baz"} = urlsplit("/foo#bar?baz"),
    {"http", "host:port", "/foo", "", "bar?baz"} =
        urlsplit("http://host:port/foo#bar?baz"),
    {"http", "host", "", "", ""} = urlsplit("http://host"),
    {"", "", "/wiki/Category:Fruit", "", ""} =
        urlsplit("/wiki/Category:Fruit"),
    ok.

urlsplit_path_test() ->
    {"/foo/bar", "", ""} = urlsplit_path("/foo/bar"),
    {"/foo", "baz", ""} = urlsplit_path("/foo?baz"),
    {"/foo", "", "bar?baz"} = urlsplit_path("/foo#bar?baz"),
    {"/foo", "", "bar?baz#wibble"} = urlsplit_path("/foo#bar?baz#wibble"),
    {"/foo", "bar", "baz"} = urlsplit_path("/foo?bar#baz"),
    {"/foo", "bar?baz", "baz"} = urlsplit_path("/foo?bar?baz#baz"),
    ok.

urlunsplit_test() ->
    "/foo#bar?baz" = urlunsplit({"", "", "/foo", "", "bar?baz"}),
    "http://host:port/foo#bar?baz" =
        urlunsplit({"http", "host:port", "/foo", "", "bar?baz"}),
    ok.

urlunsplit_path_test() ->
    "/foo/bar" = urlunsplit_path({"/foo/bar", "", ""}),
    "/foo?baz" = urlunsplit_path({"/foo", "baz", ""}),
    "/foo#bar?baz" = urlunsplit_path({"/foo", "", "bar?baz"}),
    "/foo#bar?baz#wibble" = urlunsplit_path({"/foo", "", "bar?baz#wibble"}),
    "/foo?bar#baz" = urlunsplit_path({"/foo", "bar", "baz"}),
    "/foo?bar?baz#baz" = urlunsplit_path({"/foo", "bar?baz", "baz"}),
    ok.

join_test() ->
    ?assertEqual("foo,bar,baz",
                  join(["foo", "bar", "baz"], $,)),
    ?assertEqual("foo,bar,baz",
                  join(["foo", "bar", "baz"], ",")),
    ?assertEqual("foo bar",
                  join([["foo", " bar"]], ",")),
    ?assertEqual("foo bar,baz",
                  join([["foo", " bar"], "baz"], ",")),
    ?assertEqual("foo",
                  join(["foo"], ",")),
    ?assertEqual("foobarbaz",
                  join(["foo", "bar", "baz"], "")),
    ?assertEqual("foo" ++ [<<>>] ++ "bar" ++ [<<>>] ++ "baz",
                 join(["foo", "bar", "baz"], <<>>)),
    ?assertEqual("foobar" ++ [<<"baz">>],
                 join(["foo", "bar", <<"baz">>], "")),
    ?assertEqual("",
                 join([], "any")),
    ok.

quote_plus_test() ->
    "foo" = quote_plus(foo),
    "1" = quote_plus(1),
    "1.1" = quote_plus(1.1),
    "foo" = quote_plus("foo"),
    "foo+bar" = quote_plus("foo bar"),
    "foo%0A" = quote_plus("foo\n"),
    "foo%0A" = quote_plus("foo\n"),
    "foo%3B%26%3D" = quote_plus("foo;&="),
    "foo%3B%26%3D" = quote_plus(<<"foo;&=">>),
    ok.

unquote_test() ->
    ?assertEqual("foo bar",
                 unquote("foo+bar")),
    ?assertEqual("foo bar",
                 unquote("foo%20bar")),
    ?assertEqual("foo\r\n",
                 unquote("foo%0D%0A")),
    ?assertEqual("foo\r\n",
                 unquote(<<"foo%0D%0A">>)),
    ok.

urlencode_test() ->
    "foo=bar&baz=wibble+%0D%0A&z=1" = urlencode([{foo, "bar"},
                                                 {"baz", "wibble \r\n"},
                                                 {z, 1}]),
    ok.

parse_qs_test() ->
    ?assertEqual(
       [{"foo", "bar"}, {"baz", "wibble \r\n"}, {"z", "1"}],
       parse_qs("foo=bar&baz=wibble+%0D%0a&z=1")),
    ?assertEqual(
       [{"", "bar"}, {"baz", "wibble \r\n"}, {"z", ""}],
       parse_qs("=bar&baz=wibble+%0D%0a&z=")),
    ?assertEqual(
       [{"foo", "bar"}, {"baz", "wibble \r\n"}, {"z", "1"}],
       parse_qs(<<"foo=bar&baz=wibble+%0D%0a&z=1">>)),
    ?assertEqual(
       [],
       parse_qs("")),
    ?assertEqual(
       [{"foo", ""}, {"bar", ""}, {"baz", ""}],
       parse_qs("foo;bar&baz")),
    ok.

partition_test() ->
    {"foo", "", ""} = partition("foo", "/"),
    {"foo", "/", "bar"} = partition("foo/bar", "/"),
    {"foo", "/", ""} = partition("foo/", "/"),
    {"", "/", "bar"} = partition("/bar", "/"),
    {"f", "oo/ba", "r"} = partition("foo/bar", "oo/ba"),
    ok.

safe_relative_path_test() ->
    "foo" = safe_relative_path("foo"),
    "foo/" = safe_relative_path("foo/"),
    "foo" = safe_relative_path("foo/bar/.."),
    "bar" = safe_relative_path("foo/../bar"),
    "bar/" = safe_relative_path("foo/../bar/"),
    "" = safe_relative_path("foo/.."),
    "" = safe_relative_path("foo/../"),
    undefined = safe_relative_path("/foo"),
    undefined = safe_relative_path("../foo"),
    undefined = safe_relative_path("foo/../.."),
    undefined = safe_relative_path("foo//"),
    ok.

parse_qvalues_test() ->
    [] = parse_qvalues(""),
    [{"identity", 0.0}] = parse_qvalues("identity;q=0"),
    [{"identity", 0.0}] = parse_qvalues("identity ;q=0"),
    [{"identity", 0.0}] = parse_qvalues(" identity; q =0 "),
    [{"identity", 0.0}] = parse_qvalues("identity ; q = 0"),
    [{"identity", 0.0}] = parse_qvalues("identity ; q= 0.0"),
    [{"gzip", 1.0}, {"deflate", 1.0}, {"identity", 0.0}] = parse_qvalues(
        "gzip,deflate,identity;q=0.0"
    ),
    [{"deflate", 1.0}, {"gzip", 1.0}, {"identity", 0.0}] = parse_qvalues(
        "deflate,gzip,identity;q=0.0"
    ),
    [{"gzip", 1.0}, {"deflate", 1.0}, {"gzip", 1.0}, {"identity", 0.0}] =
        parse_qvalues("gzip,deflate,gzip,identity;q=0"),
    [{"gzip", 1.0}, {"deflate", 1.0}, {"identity", 0.0}] = parse_qvalues(
        "gzip, deflate , identity; q=0.0"
    ),
    [{"gzip", 1.0}, {"deflate", 1.0}, {"identity", 0.0}] = parse_qvalues(
        "gzip; q=1, deflate;q=1.0, identity;q=0.0"
    ),
    [{"gzip", 0.5}, {"deflate", 1.0}, {"identity", 0.0}] = parse_qvalues(
        "gzip; q=0.5, deflate;q=1.0, identity;q=0"
    ),
    [{"gzip", 0.5}, {"deflate", 1.0}, {"identity", 0.0}] = parse_qvalues(
        "gzip; q=0.5, deflate , identity;q=0.0"
    ),
    [{"gzip", 0.5}, {"deflate", 0.8}, {"identity", 0.0}] = parse_qvalues(
        "gzip; q=0.5, deflate;q=0.8, identity;q=0.0"
    ),
    [{"gzip", 0.5}, {"deflate", 1.0}, {"identity", 1.0}] = parse_qvalues(
        "gzip; q=0.5,deflate,identity"
    ),
    [{"gzip", 0.5}, {"deflate", 1.0}, {"identity", 1.0}, {"identity", 1.0}] =
        parse_qvalues("gzip; q=0.5,deflate,identity, identity "),
    invalid_qvalue_string = parse_qvalues("gzip; q=1.1, deflate"),
    invalid_qvalue_string = parse_qvalues("gzip; q=0.5, deflate;q=2"),
    invalid_qvalue_string = parse_qvalues("gzip, deflate;q=AB"),
    invalid_qvalue_string = parse_qvalues("gzip; q=2.1, deflate"),
    invalid_qvalue_string = parse_qvalues("gzip; q=0.1234, deflate"),
    ok.

pick_accepted_encodings_test() ->
    ["identity"] = pick_accepted_encodings(
        [],
        ["gzip", "identity"],
        "identity"
    ),
    ["gzip", "identity"] = pick_accepted_encodings(
        [{"gzip", 1.0}],
        ["gzip", "identity"],
        "identity"
    ),
    ["identity"] = pick_accepted_encodings(
        [{"gzip", 0.0}],
        ["gzip", "identity"],
        "identity"
    ),
    ["gzip", "identity"] = pick_accepted_encodings(
        [{"gzip", 1.0}, {"deflate", 1.0}],
        ["gzip", "identity"],
        "identity"
    ),
    ["gzip", "identity"] = pick_accepted_encodings(
        [{"gzip", 0.5}, {"deflate", 1.0}],
        ["gzip", "identity"],
        "identity"
    ),
    ["identity"] = pick_accepted_encodings(
        [{"gzip", 0.0}, {"deflate", 0.0}],
        ["gzip", "identity"],
        "identity"
    ),
    ["gzip"] = pick_accepted_encodings(
        [{"gzip", 1.0}, {"deflate", 1.0}, {"identity", 0.0}],
        ["gzip", "identity"],
        "identity"
    ),
    ["gzip", "deflate", "identity"] = pick_accepted_encodings(
        [{"gzip", 1.0}, {"deflate", 1.0}],
        ["gzip", "deflate", "identity"],
        "identity"
    ),
    ["gzip", "deflate"] = pick_accepted_encodings(
        [{"gzip", 1.0}, {"deflate", 1.0}, {"identity", 0.0}],
        ["gzip", "deflate", "identity"],
        "identity"
    ),
    ["deflate", "gzip", "identity"] = pick_accepted_encodings(
        [{"gzip", 0.2}, {"deflate", 1.0}],
        ["gzip", "deflate", "identity"],
        "identity"
    ),
    ["deflate", "deflate", "gzip", "identity"] = pick_accepted_encodings(
        [{"gzip", 0.2}, {"deflate", 1.0}, {"deflate", 1.0}],
        ["gzip", "deflate", "identity"],
        "identity"
    ),
    ["deflate", "gzip", "gzip", "identity"] = pick_accepted_encodings(
        [{"gzip", 0.2}, {"deflate", 1.0}, {"gzip", 1.0}],
        ["gzip", "deflate", "identity"],
        "identity"
    ),
    ["gzip", "deflate", "gzip", "identity"] = pick_accepted_encodings(
        [{"gzip", 0.2}, {"deflate", 0.9}, {"gzip", 1.0}],
        ["gzip", "deflate", "identity"],
        "identity"
    ),
    [] = pick_accepted_encodings(
        [{"*", 0.0}],
        ["gzip", "deflate", "identity"],
        "identity"
    ),
    ["gzip", "deflate", "identity"] = pick_accepted_encodings(
        [{"*", 1.0}],
        ["gzip", "deflate", "identity"],
        "identity"
    ),
    ["gzip", "deflate", "identity"] = pick_accepted_encodings(
        [{"*", 0.6}],
        ["gzip", "deflate", "identity"],
        "identity"
    ),
    ["gzip"] = pick_accepted_encodings(
        [{"gzip", 1.0}, {"*", 0.0}],
        ["gzip", "deflate", "identity"],
        "identity"
    ),
    ["gzip", "deflate"] = pick_accepted_encodings(
        [{"gzip", 1.0}, {"deflate", 0.6}, {"*", 0.0}],
        ["gzip", "deflate", "identity"],
        "identity"
    ),
    ["deflate", "gzip"] = pick_accepted_encodings(
        [{"gzip", 0.5}, {"deflate", 1.0}, {"*", 0.0}],
        ["gzip", "deflate", "identity"],
        "identity"
    ),
    ["gzip", "identity"] = pick_accepted_encodings(
        [{"deflate", 0.0}, {"*", 1.0}],
        ["gzip", "deflate", "identity"],
        "identity"
    ),
    ["gzip", "identity"] = pick_accepted_encodings(
        [{"*", 1.0}, {"deflate", 0.0}],
        ["gzip", "deflate", "identity"],
        "identity"
    ),
    ok.

-endif.
