139de3072SChristopher Lenz%% @author Bob Ippolito <bob@mochimedia.com>
239de3072SChristopher Lenz%% @copyright 2007 Mochi Media, Inc.
339de3072SChristopher Lenz
439de3072SChristopher Lenz%% @doc Utilities for parsing and quoting.
539de3072SChristopher Lenz
639de3072SChristopher Lenz-module(mochiweb_util).
739de3072SChristopher Lenz-author('bob@mochimedia.com').
839de3072SChristopher Lenz-export([join/2, quote_plus/1, urlencode/1, parse_qs/1, unquote/1]).
939de3072SChristopher Lenz-export([path_split/1]).
1039de3072SChristopher Lenz-export([urlsplit/1, urlsplit_path/1, urlunsplit/1, urlunsplit_path/1]).
1139de3072SChristopher Lenz-export([guess_mime/1, parse_header/1]).
12*a104cbfcSFilipe David Borba Manana-export([shell_quote/1, cmd/1, cmd_string/1, cmd_port/2, cmd_status/1, cmd_status/2]).
1339de3072SChristopher Lenz-export([record_to_proplist/2, record_to_proplist/3]).
1465e850a0SChristopher Lenz-export([safe_relative_path/1, partition/2]).
15ee09a0deSJohn Christopher Anderson-export([parse_qvalues/1, pick_accepted_encodings/3]).
164b0948ddSRobert Newson-export([make_io/1]).
1739de3072SChristopher Lenz
1839de3072SChristopher Lenz-define(PERCENT, 37).  % $\%
1939de3072SChristopher Lenz-define(FULLSTOP, 46). % $\.
2039de3072SChristopher Lenz-define(IS_HEX(C), ((C >= $0 andalso C =< $9) orelse
2139de3072SChristopher Lenz                    (C >= $a andalso C =< $f) orelse
2239de3072SChristopher Lenz                    (C >= $A andalso C =< $F))).
2339de3072SChristopher Lenz-define(QS_SAFE(C), ((C >= $a andalso C =< $z) orelse
2439de3072SChristopher Lenz                     (C >= $A andalso C =< $Z) orelse
2539de3072SChristopher Lenz                     (C >= $0 andalso C =< $9) orelse
2639de3072SChristopher Lenz                     (C =:= ?FULLSTOP orelse C =:= $- orelse C =:= $~ orelse
2739de3072SChristopher Lenz                      C =:= $_))).
2839de3072SChristopher Lenz
2939de3072SChristopher Lenzhexdigit(C) when C < 10 -> $0 + C;
3039de3072SChristopher Lenzhexdigit(C) when C < 16 -> $A + (C - 10).
3139de3072SChristopher Lenz
3239de3072SChristopher Lenzunhexdigit(C) when C >= $0, C =< $9 -> C - $0;
3339de3072SChristopher Lenzunhexdigit(C) when C >= $a, C =< $f -> C - $a + 10;
3439de3072SChristopher Lenzunhexdigit(C) when C >= $A, C =< $F -> C - $A + 10.
3539de3072SChristopher Lenz
3665e850a0SChristopher Lenz%% @spec partition(String, Sep) -> {String, [], []} | {Prefix, Sep, Postfix}
3765e850a0SChristopher Lenz%% @doc Inspired by Python 2.5's str.partition:
3865e850a0SChristopher Lenz%%      partition("foo/bar", "/") = {"foo", "/", "bar"},
3965e850a0SChristopher Lenz%%      partition("foo", "/") = {"foo", "", ""}.
4065e850a0SChristopher Lenzpartition(String, Sep) ->
4165e850a0SChristopher Lenz    case partition(String, Sep, []) of
4265e850a0SChristopher Lenz        undefined ->
4365e850a0SChristopher Lenz            {String, "", ""};
4465e850a0SChristopher Lenz        Result ->
4565e850a0SChristopher Lenz            Result
4665e850a0SChristopher Lenz    end.
4765e850a0SChristopher Lenz
4865e850a0SChristopher Lenzpartition("", _Sep, _Acc) ->
4965e850a0SChristopher Lenz    undefined;
5065e850a0SChristopher Lenzpartition(S, Sep, Acc) ->
5165e850a0SChristopher Lenz    case partition2(S, Sep) of
5265e850a0SChristopher Lenz        undefined ->
5365e850a0SChristopher Lenz            [C | Rest] = S,
5465e850a0SChristopher Lenz            partition(Rest, Sep, [C | Acc]);
5565e850a0SChristopher Lenz        Rest ->
5665e850a0SChristopher Lenz            {lists:reverse(Acc), Sep, Rest}
5765e850a0SChristopher Lenz    end.
5865e850a0SChristopher Lenz
5965e850a0SChristopher Lenzpartition2(Rest, "") ->
6065e850a0SChristopher Lenz    Rest;
6165e850a0SChristopher Lenzpartition2([C | R1], [C | R2]) ->
6265e850a0SChristopher Lenz    partition2(R1, R2);
6365e850a0SChristopher Lenzpartition2(_S, _Sep) ->
6465e850a0SChristopher Lenz    undefined.
6565e850a0SChristopher Lenz
6665e850a0SChristopher Lenz
6765e850a0SChristopher Lenz
6865e850a0SChristopher Lenz%% @spec safe_relative_path(string()) -> string() | undefined
6965e850a0SChristopher Lenz%% @doc Return the reduced version of a relative path or undefined if it
7065e850a0SChristopher Lenz%%      is not safe. safe relative paths can be joined with an absolute path
715938ee30SSriram Melkote%%      and will result in a subdirectory of the absolute path. Safe paths
725938ee30SSriram Melkote%%      never contain a backslash character.
7365e850a0SChristopher Lenzsafe_relative_path("/" ++ _) ->
7465e850a0SChristopher Lenz    undefined;
7565e850a0SChristopher Lenzsafe_relative_path(P) ->
765938ee30SSriram Melkote    case string:chr(P, $\\) of
775938ee30SSriram Melkote        0 ->
785938ee30SSriram Melkote           safe_relative_path(P, []);
795938ee30SSriram Melkote        _ ->
805938ee30SSriram Melkote           undefined
815938ee30SSriram Melkote    end.
825938ee30SSriram Melkote
8365e850a0SChristopher Lenzsafe_relative_path("", Acc) ->
8465e850a0SChristopher Lenz    case Acc of
8565e850a0SChristopher Lenz        [] ->
8665e850a0SChristopher Lenz            "";
8765e850a0SChristopher Lenz        _ ->
883a4fad07SAdam Kocoloski            string:join(lists:reverse(Acc), "/")
8965e850a0SChristopher Lenz    end;
9065e850a0SChristopher Lenzsafe_relative_path(P, Acc) ->
9165e850a0SChristopher Lenz    case partition(P, "/") of
9265e850a0SChristopher Lenz        {"", "/", _} ->
9365e850a0SChristopher Lenz            %% /foo or foo//bar
9465e850a0SChristopher Lenz            undefined;
9565e850a0SChristopher Lenz        {"..", _, _} when Acc =:= [] ->
9665e850a0SChristopher Lenz            undefined;
9765e850a0SChristopher Lenz        {"..", _, Rest} ->
9865e850a0SChristopher Lenz            safe_relative_path(Rest, tl(Acc));
9965e850a0SChristopher Lenz        {Part, "/", ""} ->
10065e850a0SChristopher Lenz            safe_relative_path("", ["", Part | Acc]);
10165e850a0SChristopher Lenz        {Part, _, Rest} ->
10265e850a0SChristopher Lenz            safe_relative_path(Rest, [Part | Acc])
10365e850a0SChristopher Lenz    end.
10465e850a0SChristopher Lenz
10539de3072SChristopher Lenz%% @spec shell_quote(string()) -> string()
10639de3072SChristopher Lenz%% @doc Quote a string according to UNIX shell quoting rules, returns a string
10739de3072SChristopher Lenz%%      surrounded by double quotes.
10839de3072SChristopher Lenzshell_quote(L) ->
10939de3072SChristopher Lenz    shell_quote(L, [$\"]).
11039de3072SChristopher Lenz
11139de3072SChristopher Lenz%% @spec cmd_port([string()], Options) -> port()
11239de3072SChristopher Lenz%% @doc open_port({spawn, mochiweb_util:cmd_string(Argv)}, Options).
11339de3072SChristopher Lenzcmd_port(Argv, Options) ->
11439de3072SChristopher Lenz    open_port({spawn, cmd_string(Argv)}, Options).
11539de3072SChristopher Lenz
11639de3072SChristopher Lenz%% @spec cmd([string()]) -> string()
11739de3072SChristopher Lenz%% @doc os:cmd(cmd_string(Argv)).
11839de3072SChristopher Lenzcmd(Argv) ->
11939de3072SChristopher Lenz    os:cmd(cmd_string(Argv)).
12039de3072SChristopher Lenz
12139de3072SChristopher Lenz%% @spec cmd_string([string()]) -> string()
12239de3072SChristopher Lenz%% @doc Create a shell quoted command string from a list of arguments.
12339de3072SChristopher Lenzcmd_string(Argv) ->
1244b0948ddSRobert Newson    string:join([shell_quote(X) || X <- Argv], " ").
12539de3072SChristopher Lenz
1264b0948ddSRobert Newson%% @spec cmd_status([string()]) -> {ExitStatus::integer(), Stdout::binary()}
127*a104cbfcSFilipe David Borba Manana%% @doc Accumulate the output and exit status from the given application,
128*a104cbfcSFilipe David Borba Manana%%      will be spawned with cmd_port/2.
1294b0948ddSRobert Newsoncmd_status(Argv) ->
130*a104cbfcSFilipe David Borba Manana    cmd_status(Argv, []).
131*a104cbfcSFilipe David Borba Manana
132*a104cbfcSFilipe David Borba Manana%% @spec cmd_status([string()], [atom()]) -> {ExitStatus::integer(), Stdout::binary()}
133*a104cbfcSFilipe David Borba Manana%% @doc Accumulate the output and exit status from the given application,
134*a104cbfcSFilipe David Borba Manana%%      will be spawned with cmd_port/2.
135*a104cbfcSFilipe David Borba Mananacmd_status(Argv, Options) ->
1364b0948ddSRobert Newson    Port = cmd_port(Argv, [exit_status, stderr_to_stdout,
137*a104cbfcSFilipe David Borba Manana                           use_stdio, binary | Options]),
1384b0948ddSRobert Newson    try cmd_loop(Port, [])
1394b0948ddSRobert Newson    after catch port_close(Port)
1404b0948ddSRobert Newson    end.
1414b0948ddSRobert Newson
1424b0948ddSRobert Newson%% @spec cmd_loop(port(), list()) -> {ExitStatus::integer(), Stdout::binary()}
1434b0948ddSRobert Newson%% @doc Accumulate the output and exit status from a port.
1444b0948ddSRobert Newsoncmd_loop(Port, Acc) ->
1454b0948ddSRobert Newson    receive
1464b0948ddSRobert Newson        {Port, {exit_status, Status}} ->
1474b0948ddSRobert Newson            {Status, iolist_to_binary(lists:reverse(Acc))};
1484b0948ddSRobert Newson        {Port, {data, Data}} ->
1494b0948ddSRobert Newson            cmd_loop(Port, [Data | Acc])
1504b0948ddSRobert Newson    end.
1514b0948ddSRobert Newson
1524b0948ddSRobert Newson%% @spec join([iolist()], iolist()) -> iolist()
1534b0948ddSRobert Newson%% @doc Join a list of strings or binaries together with the given separator
1544b0948ddSRobert Newson%%      string or char or binary. The output is flattened, but may be an
1554b0948ddSRobert Newson%%      iolist() instead of a string() if any of the inputs are binary().
15639de3072SChristopher Lenzjoin([], _Separator) ->
15739de3072SChristopher Lenz    [];
15839de3072SChristopher Lenzjoin([S], _Separator) ->
15939de3072SChristopher Lenz    lists:flatten(S);
16039de3072SChristopher Lenzjoin(Strings, Separator) ->
16139de3072SChristopher Lenz    lists:flatten(revjoin(lists:reverse(Strings), Separator, [])).
16239de3072SChristopher Lenz
16339de3072SChristopher Lenzrevjoin([], _Separator, Acc) ->
16439de3072SChristopher Lenz    Acc;
16539de3072SChristopher Lenzrevjoin([S | Rest], Separator, []) ->
16639de3072SChristopher Lenz    revjoin(Rest, Separator, [S]);
16739de3072SChristopher Lenzrevjoin([S | Rest], Separator, Acc) ->
16839de3072SChristopher Lenz    revjoin(Rest, Separator, [S, Separator | Acc]).
16939de3072SChristopher Lenz
1701ba7a12cSChristopher Lenz%% @spec quote_plus(atom() | integer() | float() | string() | binary()) -> string()
17139de3072SChristopher Lenz%% @doc URL safe encoding of the given term.
17239de3072SChristopher Lenzquote_plus(Atom) when is_atom(Atom) ->
17339de3072SChristopher Lenz    quote_plus(atom_to_list(Atom));
17439de3072SChristopher Lenzquote_plus(Int) when is_integer(Int) ->
17539de3072SChristopher Lenz    quote_plus(integer_to_list(Int));
1761ba7a12cSChristopher Lenzquote_plus(Binary) when is_binary(Binary) ->
1771ba7a12cSChristopher Lenz    quote_plus(binary_to_list(Binary));
1781ba7a12cSChristopher Lenzquote_plus(Float) when is_float(Float) ->
1791ba7a12cSChristopher Lenz    quote_plus(mochinum:digits(Float));
18039de3072SChristopher Lenzquote_plus(String) ->
18139de3072SChristopher Lenz    quote_plus(String, []).
18239de3072SChristopher Lenz
18339de3072SChristopher Lenzquote_plus([], Acc) ->
18439de3072SChristopher Lenz    lists:reverse(Acc);
18539de3072SChristopher Lenzquote_plus([C | Rest], Acc) when ?QS_SAFE(C) ->
18639de3072SChristopher Lenz    quote_plus(Rest, [C | Acc]);
18739de3072SChristopher Lenzquote_plus([$\s | Rest], Acc) ->
18839de3072SChristopher Lenz    quote_plus(Rest, [$+ | Acc]);
18939de3072SChristopher Lenzquote_plus([C | Rest], Acc) ->
19039de3072SChristopher Lenz    <<Hi:4, Lo:4>> = <<C>>,
19139de3072SChristopher Lenz    quote_plus(Rest, [hexdigit(Lo), hexdigit(Hi), ?PERCENT | Acc]).
19239de3072SChristopher Lenz
19339de3072SChristopher Lenz%% @spec urlencode([{Key, Value}]) -> string()
19439de3072SChristopher Lenz%% @doc URL encode the property list.
19539de3072SChristopher Lenzurlencode(Props) ->
1964b0948ddSRobert Newson    Pairs = lists:foldr(
1974b0948ddSRobert Newson              fun ({K, V}, Acc) ->
1984b0948ddSRobert Newson                      [quote_plus(K) ++ "=" ++ quote_plus(V) | Acc]
19939de3072SChristopher Lenz              end, [], Props),
2004b0948ddSRobert Newson    string:join(Pairs, "&").
20139de3072SChristopher Lenz
20239de3072SChristopher Lenz%% @spec parse_qs(string() | binary()) -> [{Key, Value}]
20339de3072SChristopher Lenz%% @doc Parse a query string or application/x-www-form-urlencoded.
20439de3072SChristopher Lenzparse_qs(Binary) when is_binary(Binary) ->
20539de3072SChristopher Lenz    parse_qs(binary_to_list(Binary));
20639de3072SChristopher Lenzparse_qs(String) ->
20739de3072SChristopher Lenz    parse_qs(String, []).
20839de3072SChristopher Lenz
20939de3072SChristopher Lenzparse_qs([], Acc) ->
21039de3072SChristopher Lenz    lists:reverse(Acc);
21139de3072SChristopher Lenzparse_qs(String, Acc) ->
21239de3072SChristopher Lenz    {Key, Rest} = parse_qs_key(String),
21339de3072SChristopher Lenz    {Value, Rest1} = parse_qs_value(Rest),
21439de3072SChristopher Lenz    parse_qs(Rest1, [{Key, Value} | Acc]).
21539de3072SChristopher Lenz
21639de3072SChristopher Lenzparse_qs_key(String) ->
21739de3072SChristopher Lenz    parse_qs_key(String, []).
21839de3072SChristopher Lenz
21939de3072SChristopher Lenzparse_qs_key([], Acc) ->
22039de3072SChristopher Lenz    {qs_revdecode(Acc), ""};
22139de3072SChristopher Lenzparse_qs_key([$= | Rest], Acc) ->
22239de3072SChristopher Lenz    {qs_revdecode(Acc), Rest};
22339de3072SChristopher Lenzparse_qs_key(Rest=[$; | _], Acc) ->
22439de3072SChristopher Lenz    {qs_revdecode(Acc), Rest};
22539de3072SChristopher Lenzparse_qs_key(Rest=[$& | _], Acc) ->
22639de3072SChristopher Lenz    {qs_revdecode(Acc), Rest};
22739de3072SChristopher Lenzparse_qs_key([C | Rest], Acc) ->
22839de3072SChristopher Lenz    parse_qs_key(Rest, [C | Acc]).
22939de3072SChristopher Lenz
23039de3072SChristopher Lenzparse_qs_value(String) ->
23139de3072SChristopher Lenz    parse_qs_value(String, []).
23239de3072SChristopher Lenz
23339de3072SChristopher Lenzparse_qs_value([], Acc) ->
23439de3072SChristopher Lenz    {qs_revdecode(Acc), ""};
23539de3072SChristopher Lenzparse_qs_value([$; | Rest], Acc) ->
23639de3072SChristopher Lenz    {qs_revdecode(Acc), Rest};
23739de3072SChristopher Lenzparse_qs_value([$& | Rest], Acc) ->
23839de3072SChristopher Lenz    {qs_revdecode(Acc), Rest};
23939de3072SChristopher Lenzparse_qs_value([C | Rest], Acc) ->
24039de3072SChristopher Lenz    parse_qs_value(Rest, [C | Acc]).
24139de3072SChristopher Lenz
24239de3072SChristopher Lenz%% @spec unquote(string() | binary()) -> string()
24339de3072SChristopher Lenz%% @doc Unquote a URL encoded string.
24439de3072SChristopher Lenzunquote(Binary) when is_binary(Binary) ->
24539de3072SChristopher Lenz    unquote(binary_to_list(Binary));
24639de3072SChristopher Lenzunquote(String) ->
24739de3072SChristopher Lenz    qs_revdecode(lists:reverse(String)).
24839de3072SChristopher Lenz
24939de3072SChristopher Lenzqs_revdecode(S) ->
25039de3072SChristopher Lenz    qs_revdecode(S, []).
25139de3072SChristopher Lenz
25239de3072SChristopher Lenzqs_revdecode([], Acc) ->
25339de3072SChristopher Lenz    Acc;
25439de3072SChristopher Lenzqs_revdecode([$+ | Rest], Acc) ->
25539de3072SChristopher Lenz    qs_revdecode(Rest, [$\s | Acc]);
25639de3072SChristopher Lenzqs_revdecode([Lo, Hi, ?PERCENT | Rest], Acc) when ?IS_HEX(Lo), ?IS_HEX(Hi) ->
25739de3072SChristopher Lenz    qs_revdecode(Rest, [(unhexdigit(Lo) bor (unhexdigit(Hi) bsl 4)) | Acc]);
25839de3072SChristopher Lenzqs_revdecode([C | Rest], Acc) ->
25939de3072SChristopher Lenz    qs_revdecode(Rest, [C | Acc]).
26039de3072SChristopher Lenz
26139de3072SChristopher Lenz%% @spec urlsplit(Url) -> {Scheme, Netloc, Path, Query, Fragment}
26239de3072SChristopher Lenz%% @doc Return a 5-tuple, does not expand % escapes. Only supports HTTP style
26339de3072SChristopher Lenz%%      URLs.
26439de3072SChristopher Lenzurlsplit(Url) ->
26539de3072SChristopher Lenz    {Scheme, Url1} = urlsplit_scheme(Url),
26639de3072SChristopher Lenz    {Netloc, Url2} = urlsplit_netloc(Url1),
26739de3072SChristopher Lenz    {Path, Query, Fragment} = urlsplit_path(Url2),
26839de3072SChristopher Lenz    {Scheme, Netloc, Path, Query, Fragment}.
26939de3072SChristopher Lenz
27039de3072SChristopher Lenzurlsplit_scheme(Url) ->
2714b0948ddSRobert Newson    case urlsplit_scheme(Url, []) of
2724b0948ddSRobert Newson        no_scheme ->
2734b0948ddSRobert Newson            {"", Url};
2744b0948ddSRobert Newson        Res ->
2754b0948ddSRobert Newson            Res
2764b0948ddSRobert Newson    end.
27739de3072SChristopher Lenz
2784b0948ddSRobert Newsonurlsplit_scheme([C | Rest], Acc) when ((C >= $a andalso C =< $z) orelse
2794b0948ddSRobert Newson                                       (C >= $A andalso C =< $Z) orelse
2804b0948ddSRobert Newson                                       (C >= $0 andalso C =< $9) orelse
2814b0948ddSRobert Newson                                       C =:= $+ orelse C =:= $- orelse
2824b0948ddSRobert Newson                                       C =:= $.) ->
2834b0948ddSRobert Newson    urlsplit_scheme(Rest, [C | Acc]);
2844b0948ddSRobert Newsonurlsplit_scheme([$: | Rest], Acc=[_ | _]) ->
285678512dcSChristopher Lenz    {string:to_lower(lists:reverse(Acc)), Rest};
2864b0948ddSRobert Newsonurlsplit_scheme(_Rest, _Acc) ->
2874b0948ddSRobert Newson    no_scheme.
28839de3072SChristopher Lenz
28939de3072SChristopher Lenzurlsplit_netloc("//" ++ Rest) ->
29039de3072SChristopher Lenz    urlsplit_netloc(Rest, []);
29139de3072SChristopher Lenzurlsplit_netloc(Path) ->
29239de3072SChristopher Lenz    {"", Path}.
29339de3072SChristopher Lenz
2944b0948ddSRobert Newsonurlsplit_netloc("", Acc) ->
2954b0948ddSRobert Newson    {lists:reverse(Acc), ""};
29639de3072SChristopher Lenzurlsplit_netloc(Rest=[C | _], Acc) when C =:= $/; C =:= $?; C =:= $# ->
29739de3072SChristopher Lenz    {lists:reverse(Acc), Rest};
29839de3072SChristopher Lenzurlsplit_netloc([C | Rest], Acc) ->
29939de3072SChristopher Lenz    urlsplit_netloc(Rest, [C | Acc]).
30039de3072SChristopher Lenz
30139de3072SChristopher Lenz
30239de3072SChristopher Lenz%% @spec path_split(string()) -> {Part, Rest}
30339de3072SChristopher Lenz%% @doc Split a path starting from the left, as in URL traversal.
30439de3072SChristopher Lenz%%      path_split("foo/bar") = {"foo", "bar"},
30539de3072SChristopher Lenz%%      path_split("/foo/bar") = {"", "foo/bar"}.
30639de3072SChristopher Lenzpath_split(S) ->
30739de3072SChristopher Lenz    path_split(S, []).
30839de3072SChristopher Lenz
30939de3072SChristopher Lenzpath_split("", Acc) ->
31039de3072SChristopher Lenz    {lists:reverse(Acc), ""};
31139de3072SChristopher Lenzpath_split("/" ++ Rest, Acc) ->
31239de3072SChristopher Lenz    {lists:reverse(Acc), Rest};
31339de3072SChristopher Lenzpath_split([C | Rest], Acc) ->
31439de3072SChristopher Lenz    path_split(Rest, [C | Acc]).
31539de3072SChristopher Lenz
31639de3072SChristopher Lenz
31739de3072SChristopher Lenz%% @spec urlunsplit({Scheme, Netloc, Path, Query, Fragment}) -> string()
31839de3072SChristopher Lenz%% @doc Assemble a URL from the 5-tuple. Path must be absolute.
31939de3072SChristopher Lenzurlunsplit({Scheme, Netloc, Path, Query, Fragment}) ->
32039de3072SChristopher Lenz    lists:flatten([case Scheme of "" -> "";  _ -> [Scheme, "://"] end,
32139de3072SChristopher Lenz                   Netloc,
32239de3072SChristopher Lenz                   urlunsplit_path({Path, Query, Fragment})]).
32339de3072SChristopher Lenz
32439de3072SChristopher Lenz%% @spec urlunsplit_path({Path, Query, Fragment}) -> string()
32539de3072SChristopher Lenz%% @doc Assemble a URL path from the 3-tuple.
32639de3072SChristopher Lenzurlunsplit_path({Path, Query, Fragment}) ->
32739de3072SChristopher Lenz    lists:flatten([Path,
32839de3072SChristopher Lenz                   case Query of "" -> ""; _ -> [$? | Query] end,
32939de3072SChristopher Lenz                   case Fragment of "" -> ""; _ -> [$# | Fragment] end]).
33039de3072SChristopher Lenz
33139de3072SChristopher Lenz%% @spec urlsplit_path(Url) -> {Path, Query, Fragment}
33239de3072SChristopher Lenz%% @doc Return a 3-tuple, does not expand % escapes. Only supports HTTP style
33339de3072SChristopher Lenz%%      paths.
33439de3072SChristopher Lenzurlsplit_path(Path) ->
33539de3072SChristopher Lenz    urlsplit_path(Path, []).
33639de3072SChristopher Lenz
33739de3072SChristopher Lenzurlsplit_path("", Acc) ->
33839de3072SChristopher Lenz    {lists:reverse(Acc), "", ""};
33939de3072SChristopher Lenzurlsplit_path("?" ++ Rest, Acc) ->
34039de3072SChristopher Lenz    {Query, Fragment} = urlsplit_query(Rest),
34139de3072SChristopher Lenz    {lists:reverse(Acc), Query, Fragment};
34239de3072SChristopher Lenzurlsplit_path("#" ++ Rest, Acc) ->
34339de3072SChristopher Lenz    {lists:reverse(Acc), "", Rest};
34439de3072SChristopher Lenzurlsplit_path([C | Rest], Acc) ->
34539de3072SChristopher Lenz    urlsplit_path(Rest, [C | Acc]).
34639de3072SChristopher Lenz
34739de3072SChristopher Lenzurlsplit_query(Query) ->
34839de3072SChristopher Lenz    urlsplit_query(Query, []).
34939de3072SChristopher Lenz
35039de3072SChristopher Lenzurlsplit_query("", Acc) ->
35139de3072SChristopher Lenz    {lists:reverse(Acc), ""};
35239de3072SChristopher Lenzurlsplit_query("#" ++ Rest, Acc) ->
35339de3072SChristopher Lenz    {lists:reverse(Acc), Rest};
35439de3072SChristopher Lenzurlsplit_query([C | Rest], Acc) ->
35539de3072SChristopher Lenz    urlsplit_query(Rest, [C | Acc]).
35639de3072SChristopher Lenz
35739de3072SChristopher Lenz%% @spec guess_mime(string()) -> string()
35839de3072SChristopher Lenz%% @doc  Guess the mime type of a file by the extension of its filename.
35939de3072SChristopher Lenzguess_mime(File) ->
3604b0948ddSRobert Newson    case mochiweb_mime:from_extension(filename:extension(File)) of
3614b0948ddSRobert Newson        undefined ->
36239de3072SChristopher Lenz            "text/plain";
3634b0948ddSRobert Newson        Mime ->
3644b0948ddSRobert Newson            Mime
36539de3072SChristopher Lenz    end.
36639de3072SChristopher Lenz
36739de3072SChristopher Lenz%% @spec parse_header(string()) -> {Type, [{K, V}]}
36839de3072SChristopher Lenz%% @doc  Parse a Content-Type like header, return the main Content-Type
36939de3072SChristopher Lenz%%       and a property list of options.
37039de3072SChristopher Lenzparse_header(String) ->
37139de3072SChristopher Lenz    %% TODO: This is exactly as broken as Python's cgi module.
37239de3072SChristopher Lenz    %%       Should parse properly like mochiweb_cookies.
37339de3072SChristopher Lenz    [Type | Parts] = [string:strip(S) || S <- string:tokens(String, ";")],
37439de3072SChristopher Lenz    F = fun (S, Acc) ->
37539de3072SChristopher Lenz                case lists:splitwith(fun (C) -> C =/= $= end, S) of
37639de3072SChristopher Lenz                    {"", _} ->
37739de3072SChristopher Lenz                        %% Skip anything with no name
37839de3072SChristopher Lenz                        Acc;
37939de3072SChristopher Lenz                    {_, ""} ->
38039de3072SChristopher Lenz                        %% Skip anything with no value
38139de3072SChristopher Lenz                        Acc;
38239de3072SChristopher Lenz                    {Name, [$\= | Value]} ->
383678512dcSChristopher Lenz                        [{string:to_lower(string:strip(Name)),
38439de3072SChristopher Lenz                          unquote_header(string:strip(Value))} | Acc]
38539de3072SChristopher Lenz                end
38639de3072SChristopher Lenz        end,
387678512dcSChristopher Lenz    {string:to_lower(Type),
38839de3072SChristopher Lenz     lists:foldr(F, [], Parts)}.
38939de3072SChristopher Lenz
39039de3072SChristopher Lenzunquote_header("\"" ++ Rest) ->
39139de3072SChristopher Lenz    unquote_header(Rest, []);
39239de3072SChristopher Lenzunquote_header(S) ->
39339de3072SChristopher Lenz    S.
39439de3072SChristopher Lenz
39539de3072SChristopher Lenzunquote_header("", Acc) ->
39639de3072SChristopher Lenz    lists:reverse(Acc);
39739de3072SChristopher Lenzunquote_header("\"", Acc) ->
39839de3072SChristopher Lenz    lists:reverse(Acc);
39939de3072SChristopher Lenzunquote_header([$\\, C | Rest], Acc) ->
40039de3072SChristopher Lenz    unquote_header(Rest, [C | Acc]);
40139de3072SChristopher Lenzunquote_header([C | Rest], Acc) ->
40239de3072SChristopher Lenz    unquote_header(Rest, [C | Acc]).
40339de3072SChristopher Lenz
40439de3072SChristopher Lenz%% @spec record_to_proplist(Record, Fields) -> proplist()
40539de3072SChristopher Lenz%% @doc calls record_to_proplist/3 with a default TypeKey of '__record'
40639de3072SChristopher Lenzrecord_to_proplist(Record, Fields) ->
40739de3072SChristopher Lenz    record_to_proplist(Record, Fields, '__record').
40839de3072SChristopher Lenz
40939de3072SChristopher Lenz%% @spec record_to_proplist(Record, Fields, TypeKey) -> proplist()
41039de3072SChristopher Lenz%% @doc Return a proplist of the given Record with each field in the
41139de3072SChristopher Lenz%%      Fields list set as a key with the corresponding value in the Record.
41239de3072SChristopher Lenz%%      TypeKey is the key that is used to store the record type
41339de3072SChristopher Lenz%%      Fields should be obtained by calling record_info(fields, record_type)
41439de3072SChristopher Lenz%%      where record_type is the record type of Record
41539de3072SChristopher Lenzrecord_to_proplist(Record, Fields, TypeKey)
4163a4fad07SAdam Kocoloski  when tuple_size(Record) - 1 =:= length(Fields) ->
41739de3072SChristopher Lenz    lists:zip([TypeKey | Fields], tuple_to_list(Record)).
41839de3072SChristopher Lenz
41939de3072SChristopher Lenz
42039de3072SChristopher Lenzshell_quote([], Acc) ->
42139de3072SChristopher Lenz    lists:reverse([$\" | Acc]);
42239de3072SChristopher Lenzshell_quote([C | Rest], Acc) when C =:= $\" orelse C =:= $\` orelse
42339de3072SChristopher Lenz                                  C =:= $\\ orelse C =:= $\$ ->
42439de3072SChristopher Lenz    shell_quote(Rest, [C, $\\ | Acc]);
42539de3072SChristopher Lenzshell_quote([C | Rest], Acc) ->
42639de3072SChristopher Lenz    shell_quote(Rest, [C | Acc]).
42739de3072SChristopher Lenz
4284b0948ddSRobert Newson%% @spec parse_qvalues(string()) -> [qvalue()] | invalid_qvalue_string
4290264c51dSFilipe David Borba Manana%% @type qvalue() = {media_type() | encoding() , float()}.
4300264c51dSFilipe David Borba Manana%% @type media_type() = string().
4314b0948ddSRobert Newson%% @type encoding() = string().
432ee09a0deSJohn Christopher Anderson%%
433ee09a0deSJohn Christopher Anderson%% @doc Parses a list (given as a string) of elements with Q values associated
434ee09a0deSJohn Christopher Anderson%%      to them. Elements are separated by commas and each element is separated
435ee09a0deSJohn Christopher Anderson%%      from its Q value by a semicolon. Q values are optional but when missing
436ee09a0deSJohn Christopher Anderson%%      the value of an element is considered as 1.0. A Q value is always in the
437ee09a0deSJohn Christopher Anderson%%      range [0.0, 1.0]. A Q value list is used for example as the value of the
4380264c51dSFilipe David Borba Manana%%      HTTP "Accept" and "Accept-Encoding" headers.
439ee09a0deSJohn Christopher Anderson%%
440ee09a0deSJohn Christopher Anderson%%      Q values are described in section 2.9 of the RFC 2616 (HTTP 1.1).
441ee09a0deSJohn Christopher Anderson%%
442ee09a0deSJohn Christopher Anderson%%      Example:
443ee09a0deSJohn Christopher Anderson%%
444ee09a0deSJohn Christopher Anderson%%      parse_qvalues("gzip; q=0.5, deflate, identity;q=0.0") ->
445ee09a0deSJohn Christopher Anderson%%          [{"gzip", 0.5}, {"deflate", 1.0}, {"identity", 0.0}]
446ee09a0deSJohn Christopher Anderson%%
447ee09a0deSJohn Christopher Andersonparse_qvalues(QValuesStr) ->
448ee09a0deSJohn Christopher Anderson    try
449ee09a0deSJohn Christopher Anderson        lists:map(
450ee09a0deSJohn Christopher Anderson            fun(Pair) ->
4510264c51dSFilipe David Borba Manana                [Type | Params] = string:tokens(Pair, ";"),
4520264c51dSFilipe David Borba Manana                NormParams = normalize_media_params(Params),
4530264c51dSFilipe David Borba Manana                {Q, NonQParams} = extract_q(NormParams),
4540264c51dSFilipe David Borba Manana                {string:join([string:strip(Type) | NonQParams], ";"), Q}
4550264c51dSFilipe David Borba Manana            end,
4560264c51dSFilipe David Borba Manana            string:tokens(string:to_lower(QValuesStr), ",")
4570264c51dSFilipe David Borba Manana        )
4580264c51dSFilipe David Borba Manana    catch
4590264c51dSFilipe David Borba Manana        _Type:_Error ->
4600264c51dSFilipe David Borba Manana            invalid_qvalue_string
4610264c51dSFilipe David Borba Manana    end.
4620264c51dSFilipe David Borba Manana
4630264c51dSFilipe David Borba Manananormalize_media_params(Params) ->
4640264c51dSFilipe David Borba Manana    {ok, Re} = re:compile("\\s"),
4650264c51dSFilipe David Borba Manana    normalize_media_params(Re, Params, []).
4660264c51dSFilipe David Borba Manana
4670264c51dSFilipe David Borba Manananormalize_media_params(_Re, [], Acc) ->
4680264c51dSFilipe David Borba Manana    lists:reverse(Acc);
4690264c51dSFilipe David Borba Manananormalize_media_params(Re, [Param | Rest], Acc) ->
4700264c51dSFilipe David Borba Manana    NormParam = re:replace(Param, Re, "", [global, {return, list}]),
4710264c51dSFilipe David Borba Manana    normalize_media_params(Re, Rest, [NormParam | Acc]).
4720264c51dSFilipe David Borba Manana
4730264c51dSFilipe David Borba Mananaextract_q(NormParams) ->
4740264c51dSFilipe David Borba Manana    {ok, KVRe} = re:compile("^([^=]+)=([^=]+)$"),
4750264c51dSFilipe David Borba Manana    {ok, QRe} = re:compile("^((?:0|1)(?:\\.\\d{1,3})?)$"),
4760264c51dSFilipe David Borba Manana    extract_q(KVRe, QRe, NormParams, []).
4770264c51dSFilipe David Borba Manana
4780264c51dSFilipe David Borba Mananaextract_q(_KVRe, _QRe, [], Acc) ->
4790264c51dSFilipe David Borba Manana    {1.0, lists:reverse(Acc)};
4800264c51dSFilipe David Borba Mananaextract_q(KVRe, QRe, [Param | Rest], Acc) ->
4810264c51dSFilipe David Borba Manana    case re:run(Param, KVRe, [{capture, [1, 2], list}]) of
4820264c51dSFilipe David Borba Manana        {match, [Name, Value]} ->
4830264c51dSFilipe David Borba Manana            case Name of
4840264c51dSFilipe David Borba Manana            "q" ->
4850264c51dSFilipe David Borba Manana                {match, [Q]} = re:run(Value, QRe, [{capture, [1], list}]),
486ee09a0deSJohn Christopher Anderson                QVal = case Q of
487ee09a0deSJohn Christopher Anderson                    "0" ->
488ee09a0deSJohn Christopher Anderson                        0.0;
489ee09a0deSJohn Christopher Anderson                    "1" ->
490ee09a0deSJohn Christopher Anderson                        1.0;
491ee09a0deSJohn Christopher Anderson                    Else ->
492ee09a0deSJohn Christopher Anderson                        list_to_float(Else)
493ee09a0deSJohn Christopher Anderson                end,
494ee09a0deSJohn Christopher Anderson                case QVal < 0.0 orelse QVal > 1.0 of
495ee09a0deSJohn Christopher Anderson                false ->
4960264c51dSFilipe David Borba Manana                    {QVal, lists:reverse(Acc) ++ Rest}
4970264c51dSFilipe David Borba Manana                end;
4980264c51dSFilipe David Borba Manana            _ ->
4990264c51dSFilipe David Borba Manana                extract_q(KVRe, QRe, Rest, [Param | Acc])
500ee09a0deSJohn Christopher Anderson            end
501ee09a0deSJohn Christopher Anderson    end.
502ee09a0deSJohn Christopher Anderson
5034b0948ddSRobert Newson%% @spec pick_accepted_encodings([qvalue()], [encoding()], encoding()) ->
504ee09a0deSJohn Christopher Anderson%%    [encoding()]
505ee09a0deSJohn Christopher Anderson%%
506ee09a0deSJohn Christopher Anderson%% @doc Determines which encodings specified in the given Q values list are
507ee09a0deSJohn Christopher Anderson%%      valid according to a list of supported encodings and a default encoding.
508ee09a0deSJohn Christopher Anderson%%
509ee09a0deSJohn Christopher Anderson%%      The returned list of encodings is sorted, descendingly, according to the
510ee09a0deSJohn Christopher Anderson%%      Q values of the given list. The last element of this list is the given
511ee09a0deSJohn Christopher Anderson%%      default encoding unless this encoding is explicitily or implicitily
512ee09a0deSJohn Christopher Anderson%%      marked with a Q value of 0.0 in the given Q values list.
513ee09a0deSJohn Christopher Anderson%%      Note: encodings with the same Q value are kept in the same order as
514ee09a0deSJohn Christopher Anderson%%            found in the input Q values list.
515ee09a0deSJohn Christopher Anderson%%
516ee09a0deSJohn Christopher Anderson%%      This encoding picking process is described in section 14.3 of the
517ee09a0deSJohn Christopher Anderson%%      RFC 2616 (HTTP 1.1).
518ee09a0deSJohn Christopher Anderson%%
519ee09a0deSJohn Christopher Anderson%%      Example:
520ee09a0deSJohn Christopher Anderson%%
521ee09a0deSJohn Christopher Anderson%%      pick_accepted_encodings(
522ee09a0deSJohn Christopher Anderson%%          [{"gzip", 0.5}, {"deflate", 1.0}],
523ee09a0deSJohn Christopher Anderson%%          ["gzip", "identity"],
524ee09a0deSJohn Christopher Anderson%%          "identity"
525ee09a0deSJohn Christopher Anderson%%      ) ->
526ee09a0deSJohn Christopher Anderson%%          ["gzip", "identity"]
527ee09a0deSJohn Christopher Anderson%%
528ee09a0deSJohn Christopher Andersonpick_accepted_encodings(AcceptedEncs, SupportedEncs, DefaultEnc) ->
529ee09a0deSJohn Christopher Anderson    SortedQList = lists:reverse(
530ee09a0deSJohn Christopher Anderson        lists:sort(fun({_, Q1}, {_, Q2}) -> Q1 < Q2 end, AcceptedEncs)
531ee09a0deSJohn Christopher Anderson    ),
532ee09a0deSJohn Christopher Anderson    {Accepted, Refused} = lists:foldr(
533ee09a0deSJohn Christopher Anderson        fun({E, Q}, {A, R}) ->
534ee09a0deSJohn Christopher Anderson            case Q > 0.0 of
535ee09a0deSJohn Christopher Anderson                true ->
536ee09a0deSJohn Christopher Anderson                    {[E | A], R};
537ee09a0deSJohn Christopher Anderson                false ->
538ee09a0deSJohn Christopher Anderson                    {A, [E | R]}
539ee09a0deSJohn Christopher Anderson            end
540ee09a0deSJohn Christopher Anderson        end,
541ee09a0deSJohn Christopher Anderson        {[], []},
542ee09a0deSJohn Christopher Anderson        SortedQList
543ee09a0deSJohn Christopher Anderson    ),
544ee09a0deSJohn Christopher Anderson    Refused1 = lists:foldr(
545ee09a0deSJohn Christopher Anderson        fun(Enc, Acc) ->
546ee09a0deSJohn Christopher Anderson            case Enc of
547ee09a0deSJohn Christopher Anderson                "*" ->
548ee09a0deSJohn Christopher Anderson                    lists:subtract(SupportedEncs, Accepted) ++ Acc;
549ee09a0deSJohn Christopher Anderson                _ ->
550ee09a0deSJohn Christopher Anderson                    [Enc | Acc]
551ee09a0deSJohn Christopher Anderson            end
552ee09a0deSJohn Christopher Anderson        end,
553ee09a0deSJohn Christopher Anderson        [],
554ee09a0deSJohn Christopher Anderson        Refused
555ee09a0deSJohn Christopher Anderson    ),
556ee09a0deSJohn Christopher Anderson    Accepted1 = lists:foldr(
557ee09a0deSJohn Christopher Anderson        fun(Enc, Acc) ->
558ee09a0deSJohn Christopher Anderson            case Enc of
559ee09a0deSJohn Christopher Anderson                "*" ->
560ee09a0deSJohn Christopher Anderson                    lists:subtract(SupportedEncs, Accepted ++ Refused1) ++ Acc;
561ee09a0deSJohn Christopher Anderson                _ ->
562ee09a0deSJohn Christopher Anderson                    [Enc | Acc]
563ee09a0deSJohn Christopher Anderson            end
564ee09a0deSJohn Christopher Anderson        end,
565ee09a0deSJohn Christopher Anderson        [],
566ee09a0deSJohn Christopher Anderson        Accepted
567ee09a0deSJohn Christopher Anderson    ),
568ee09a0deSJohn Christopher Anderson    Accepted2 = case lists:member(DefaultEnc, Accepted1) of
569ee09a0deSJohn Christopher Anderson        true ->
570ee09a0deSJohn Christopher Anderson            Accepted1;
571ee09a0deSJohn Christopher Anderson        false ->
572ee09a0deSJohn Christopher Anderson            Accepted1 ++ [DefaultEnc]
573ee09a0deSJohn Christopher Anderson    end,
574ee09a0deSJohn Christopher Anderson    [E || E <- Accepted2, lists:member(E, SupportedEncs),
575ee09a0deSJohn Christopher Anderson        not lists:member(E, Refused1)].
576ee09a0deSJohn Christopher Anderson
5774b0948ddSRobert Newsonmake_io(Atom) when is_atom(Atom) ->
5784b0948ddSRobert Newson    atom_to_list(Atom);
5794b0948ddSRobert Newsonmake_io(Integer) when is_integer(Integer) ->
5804b0948ddSRobert Newson    integer_to_list(Integer);
5814b0948ddSRobert Newsonmake_io(Io) when is_list(Io); is_binary(Io) ->
5824b0948ddSRobert Newson    Io.
5834b0948ddSRobert Newson
5844b0948ddSRobert Newson%%
5854b0948ddSRobert Newson%% Tests
5864b0948ddSRobert Newson%%
5874b0948ddSRobert Newson-ifdef(TEST).
588*a104cbfcSFilipe David Borba Manana-include_lib("eunit/include/eunit.hrl").
5894b0948ddSRobert Newson
5904b0948ddSRobert Newsonmake_io_test() ->
5914b0948ddSRobert Newson    ?assertEqual(
5924b0948ddSRobert Newson       <<"atom">>,
5934b0948ddSRobert Newson       iolist_to_binary(make_io(atom))),
5944b0948ddSRobert Newson    ?assertEqual(
5954b0948ddSRobert Newson       <<"20">>,
5964b0948ddSRobert Newson       iolist_to_binary(make_io(20))),
5974b0948ddSRobert Newson    ?assertEqual(
5984b0948ddSRobert Newson       <<"list">>,
5994b0948ddSRobert Newson       iolist_to_binary(make_io("list"))),
6004b0948ddSRobert Newson    ?assertEqual(
6014b0948ddSRobert Newson       <<"binary">>,
6024b0948ddSRobert Newson       iolist_to_binary(make_io(<<"binary">>))),
60339de3072SChristopher Lenz    ok.
60439de3072SChristopher Lenz
6054b0948ddSRobert Newson-record(test_record, {field1=f1, field2=f2}).
6064b0948ddSRobert Newsonrecord_to_proplist_test() ->
6074b0948ddSRobert Newson    ?assertEqual(
6084b0948ddSRobert Newson       [{'__record', test_record},
6094b0948ddSRobert Newson        {field1, f1},
6104b0948ddSRobert Newson        {field2, f2}],
6114b0948ddSRobert Newson       record_to_proplist(#test_record{}, record_info(fields, test_record))),
6124b0948ddSRobert Newson    ?assertEqual(
6134b0948ddSRobert Newson       [{'typekey', test_record},
6144b0948ddSRobert Newson        {field1, f1},
6154b0948ddSRobert Newson        {field2, f2}],
6164b0948ddSRobert Newson       record_to_proplist(#test_record{},
6174b0948ddSRobert Newson                          record_info(fields, test_record),
6184b0948ddSRobert Newson                          typekey)),
61939de3072SChristopher Lenz    ok.
62039de3072SChristopher Lenz
6214b0948ddSRobert Newsonshell_quote_test() ->
6224b0948ddSRobert Newson    ?assertEqual(
6234b0948ddSRobert Newson       "\"foo \\$bar\\\"\\`' baz\"",
6244b0948ddSRobert Newson       shell_quote("foo $bar\"`' baz")),
62539de3072SChristopher Lenz    ok.
62639de3072SChristopher Lenz
6274b0948ddSRobert Newsoncmd_port_test_spool(Port, Acc) ->
6284b0948ddSRobert Newson    receive
6294b0948ddSRobert Newson        {Port, eof} ->
6304b0948ddSRobert Newson            Acc;
6314b0948ddSRobert Newson        {Port, {data, {eol, Data}}} ->
6324b0948ddSRobert Newson            cmd_port_test_spool(Port, ["\n", Data | Acc]);
6334b0948ddSRobert Newson        {Port, Unknown} ->
6344b0948ddSRobert Newson            throw({unknown, Unknown})
635ef24b1d1SRobert Newson    after 1000 ->
6364b0948ddSRobert Newson            throw(timeout)
6374b0948ddSRobert Newson    end.
6384b0948ddSRobert Newson
6394b0948ddSRobert Newsoncmd_port_test() ->
6404b0948ddSRobert Newson    Port = cmd_port(["echo", "$bling$ `word`!"],
6414b0948ddSRobert Newson                    [eof, stream, {line, 4096}]),
6424b0948ddSRobert Newson    Res = try lists:append(lists:reverse(cmd_port_test_spool(Port, [])))
6434b0948ddSRobert Newson          after catch port_close(Port)
6444b0948ddSRobert Newson          end,
6454b0948ddSRobert Newson    self() ! {Port, wtf},
6464b0948ddSRobert Newson    try cmd_port_test_spool(Port, [])
6474b0948ddSRobert Newson    catch throw:{unknown, wtf} -> ok
6484b0948ddSRobert Newson    end,
6494b0948ddSRobert Newson    try cmd_port_test_spool(Port, [])
6504b0948ddSRobert Newson    catch throw:timeout -> ok
6514b0948ddSRobert Newson    end,
6524b0948ddSRobert Newson    ?assertEqual(
6534b0948ddSRobert Newson       "$bling$ `word`!\n",
6544b0948ddSRobert Newson       Res).
6554b0948ddSRobert Newson
6564b0948ddSRobert Newsoncmd_test() ->
6574b0948ddSRobert Newson    ?assertEqual(
6584b0948ddSRobert Newson       "$bling$ `word`!\n",
6594b0948ddSRobert Newson       cmd(["echo", "$bling$ `word`!"])),
66039de3072SChristopher Lenz    ok.
66139de3072SChristopher Lenz
6624b0948ddSRobert Newsoncmd_string_test() ->
6634b0948ddSRobert Newson    ?assertEqual(
6644b0948ddSRobert Newson       "\"echo\" \"\\$bling\\$ \\`word\\`!\"",
6654b0948ddSRobert Newson       cmd_string(["echo", "$bling$ `word`!"])),
66639de3072SChristopher Lenz    ok.
66739de3072SChristopher Lenz
6684b0948ddSRobert Newsoncmd_status_test() ->
6694b0948ddSRobert Newson    ?assertEqual(
6704b0948ddSRobert Newson       {0, <<"$bling$ `word`!\n">>},
6714b0948ddSRobert Newson       cmd_status(["echo", "$bling$ `word`!"])),
6724b0948ddSRobert Newson    ok.
6734b0948ddSRobert Newson
6744b0948ddSRobert Newson
6754b0948ddSRobert Newsonparse_header_test() ->
6764b0948ddSRobert Newson    ?assertEqual(
6774b0948ddSRobert Newson       {"multipart/form-data", [{"boundary", "AaB03x"}]},
6784b0948ddSRobert Newson       parse_header("multipart/form-data; boundary=AaB03x")),
6794b0948ddSRobert Newson    %% This tests (currently) intentionally broken behavior
6804b0948ddSRobert Newson    ?assertEqual(
6814b0948ddSRobert Newson       {"multipart/form-data",
6824b0948ddSRobert Newson        [{"b", ""},
6834b0948ddSRobert Newson         {"cgi", "is"},
6844b0948ddSRobert Newson         {"broken", "true\"e"}]},
6854b0948ddSRobert Newson       parse_header("multipart/form-data;b=;cgi=\"i\\s;broken=true\"e;=z;z")),
6864b0948ddSRobert Newson    ok.
6874b0948ddSRobert Newson
6884b0948ddSRobert Newsonguess_mime_test() ->
68939de3072SChristopher Lenz    "text/plain" = guess_mime(""),
69039de3072SChristopher Lenz    "text/plain" = guess_mime(".text"),
69139de3072SChristopher Lenz    "application/zip" = guess_mime(".zip"),
69239de3072SChristopher Lenz    "application/zip" = guess_mime("x.zip"),
69339de3072SChristopher Lenz    "text/html" = guess_mime("x.html"),
69439de3072SChristopher Lenz    "application/xhtml+xml" = guess_mime("x.xhtml"),
69539de3072SChristopher Lenz    ok.
69639de3072SChristopher Lenz
6974b0948ddSRobert Newsonpath_split_test() ->
69839de3072SChristopher Lenz    {"", "foo/bar"} = path_split("/foo/bar"),
69939de3072SChristopher Lenz    {"foo", "bar"} = path_split("foo/bar"),
70039de3072SChristopher Lenz    {"bar", ""} = path_split("bar"),
70139de3072SChristopher Lenz    ok.
70239de3072SChristopher Lenz
7034b0948ddSRobert Newsonurlsplit_test() ->
70439de3072SChristopher Lenz    {"", "", "/foo", "", "bar?baz"} = urlsplit("/foo#bar?baz"),
70539de3072SChristopher Lenz    {"http", "host:port", "/foo", "", "bar?baz"} =
70639de3072SChristopher Lenz        urlsplit("http://host:port/foo#bar?baz"),
7074b0948ddSRobert Newson    {"http", "host", "", "", ""} = urlsplit("http://host"),
7084b0948ddSRobert Newson    {"", "", "/wiki/Category:Fruit", "", ""} =
7094b0948ddSRobert Newson        urlsplit("/wiki/Category:Fruit"),
71039de3072SChristopher Lenz    ok.
71139de3072SChristopher Lenz
7124b0948ddSRobert Newsonurlsplit_path_test() ->
71339de3072SChristopher Lenz    {"/foo/bar", "", ""} = urlsplit_path("/foo/bar"),
71439de3072SChristopher Lenz    {"/foo", "baz", ""} = urlsplit_path("/foo?baz"),
71539de3072SChristopher Lenz    {"/foo", "", "bar?baz"} = urlsplit_path("/foo#bar?baz"),
716