blob: b5134d31b46d6d1886658a1d6a277ab6ba2225f0 [file] [log] [blame]
%% @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]).
-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([test/0]).
-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 paths
%% never contain a backslash character.
safe_relative_path("/" ++ _) ->
undefined;
safe_relative_path(P) ->
case string:chr(P, $\\) of
0 ->
safe_relative_path(P, []);
_ ->
undefined
end.
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) ->
join([shell_quote(X) || X <- Argv], " ").
%% @spec join([string()], Separator) -> string()
%% @doc Join a list of strings together with the given separator
%% string or char.
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) ->
RevPairs = lists:foldl(fun ({K, V}, Acc) ->
[[quote_plus(K), $=, quote_plus(V)] | Acc]
end, [], Props),
lists:flatten(revjoin(RevPairs, $&, [])).
%% @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) ->
urlsplit_scheme(Url, []).
urlsplit_scheme([], Acc) ->
{"", lists:reverse(Acc)};
urlsplit_scheme(":" ++ Rest, Acc) ->
{string:to_lower(lists:reverse(Acc)), Rest};
urlsplit_scheme([C | Rest], Acc) ->
urlsplit_scheme(Rest, [C | Acc]).
urlsplit_netloc("//" ++ Rest) ->
urlsplit_netloc(Rest, []);
urlsplit_netloc(Path) ->
{"", Path}.
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 filename:extension(File) of
".html" ->
"text/html";
".xhtml" ->
"application/xhtml+xml";
".xml" ->
"application/xml";
".css" ->
"text/css";
".js" ->
"application/x-javascript";
".jpg" ->
"image/jpeg";
".gif" ->
"image/gif";
".png" ->
"image/png";
".swf" ->
"application/x-shockwave-flash";
".zip" ->
"application/zip";
".bz2" ->
"application/x-bzip2";
".gz" ->
"application/x-gzip";
".tar" ->
"application/x-tar";
".tgz" ->
"application/x-gzip";
".txt" ->
"text/plain";
".doc" ->
"application/msword";
".pdf" ->
"application/pdf";
".xls" ->
"application/vnd.ms-excel";
".rtf" ->
"application/rtf";
".mov" ->
"video/quicktime";
".mp3" ->
"audio/mpeg";
".z" ->
"application/x-compress";
".wav" ->
"audio/x-wav";
".ico" ->
"image/x-icon";
".bmp" ->
"image/bmp";
".m4a" ->
"audio/mpeg";
".m3u" ->
"audio/x-mpegurl";
".exe" ->
"application/octet-stream";
".csv" ->
"text/csv";
_ ->
"text/plain"
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()] | error()
%% @type qvalue() -> {element(), q()}
%% @type element() -> string()
%% @type q() -> 0.0 .. 1.0
%% @type error() -> invalid_qvalue_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(qvalues(), [encoding()], encoding()) ->
%% [encoding()]
%% @type qvalues() -> [ {encoding(), q()} ]
%% @type encoding() -> string()
%% @type q() -> 0.0 .. 1.0
%%
%% @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)].
test() ->
test_join(),
test_quote_plus(),
test_unquote(),
test_urlencode(),
test_parse_qs(),
test_urlsplit_path(),
test_urlunsplit_path(),
test_urlsplit(),
test_urlunsplit(),
test_path_split(),
test_guess_mime(),
test_parse_header(),
test_shell_quote(),
test_cmd(),
test_cmd_string(),
test_partition(),
test_safe_relative_path(),
test_parse_qvalues(),
test_pick_accepted_encodings(),
ok.
test_shell_quote() ->
"\"foo \\$bar\\\"\\`' baz\"" = shell_quote("foo $bar\"`' baz"),
ok.
test_cmd() ->
"$bling$ `word`!\n" = cmd(["echo", "$bling$ `word`!"]),
ok.
test_cmd_string() ->
"\"echo\" \"\\$bling\\$ \\`word\\`!\"" = cmd_string(["echo", "$bling$ `word`!"]),
ok.
test_parse_header() ->
{"multipart/form-data", [{"boundary", "AaB03x"}]} =
parse_header("multipart/form-data; boundary=AaB03x"),
ok.
test_guess_mime() ->
"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.
test_path_split() ->
{"", "foo/bar"} = path_split("/foo/bar"),
{"foo", "bar"} = path_split("foo/bar"),
{"bar", ""} = path_split("bar"),
ok.
test_urlsplit() ->
{"", "", "/foo", "", "bar?baz"} = urlsplit("/foo#bar?baz"),
{"http", "host:port", "/foo", "", "bar?baz"} =
urlsplit("http://host:port/foo#bar?baz"),
ok.
test_urlsplit_path() ->
{"/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.
test_urlunsplit() ->
"/foo#bar?baz" = urlunsplit({"", "", "/foo", "", "bar?baz"}),
"http://host:port/foo#bar?baz" =
urlunsplit({"http", "host:port", "/foo", "", "bar?baz"}),
ok.
test_urlunsplit_path() ->
"/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.
test_join() ->
"foo,bar,baz" = join(["foo", "bar", "baz"], $,),
"foo,bar,baz" = join(["foo", "bar", "baz"], ","),
"foo bar" = join([["foo", " bar"]], ","),
"foo bar,baz" = join([["foo", " bar"], "baz"], ","),
"foo" = join(["foo"], ","),
"foobarbaz" = join(["foo", "bar", "baz"], ""),
ok.
test_quote_plus() ->
"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;&="),
ok.
test_unquote() ->
"foo bar" = unquote("foo+bar"),
"foo bar" = unquote("foo%20bar"),
"foo\r\n" = unquote("foo%0D%0A"),
ok.
test_urlencode() ->
"foo=bar&baz=wibble+%0D%0A&z=1" = urlencode([{foo, "bar"},
{"baz", "wibble \r\n"},
{z, 1}]),
ok.
test_parse_qs() ->
[{"foo", "bar"}, {"baz", "wibble \r\n"}, {"z", "1"}] =
parse_qs("foo=bar&baz=wibble+%0D%0A&z=1"),
ok.
test_partition() ->
{"foo", "", ""} = partition("foo", "/"),
{"foo", "/", "bar"} = partition("foo/bar", "/"),
{"foo", "/", ""} = partition("foo/", "/"),
{"", "/", "bar"} = partition("/bar", "/"),
{"f", "oo/ba", "r"} = partition("foo/bar", "oo/ba"),
ok.
test_safe_relative_path() ->
"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//"),
undefined = safe_relative_path("foo\\bar"),
ok.
test_parse_qvalues() ->
[] = 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"),
ok.
test_pick_accepted_encodings() ->
["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.