1%% @author Bob Ippolito <bob@mochimedia.com>
2%% @copyright 2007 Mochi Media, Inc.
3
4%% @doc Utilities for parsing and quoting.
5
6-module(mochiweb_util).
7-author('bob@mochimedia.com').
8-export([join/2, quote_plus/1, urlencode/1, parse_qs/1, unquote/1]).
9-export([path_split/1]).
10-export([urlsplit/1, urlsplit_path/1, urlunsplit/1, urlunsplit_path/1]).
11-export([guess_mime/1, parse_header/1]).
12-export([shell_quote/1, cmd/1, cmd_string/1, cmd_port/2, cmd_status/1]).
13-export([record_to_proplist/2, record_to_proplist/3]).
14-export([safe_relative_path/1, partition/2]).
15-export([parse_qvalues/1, pick_accepted_encodings/3]).
16-export([make_io/1]).
17
18-define(PERCENT, 37).  % $\%
19-define(FULLSTOP, 46). % $\.
20-define(IS_HEX(C), ((C >= $0 andalso C =< $9) orelse
21                    (C >= $a andalso C =< $f) orelse
22                    (C >= $A andalso C =< $F))).
23-define(QS_SAFE(C), ((C >= $a andalso C =< $z) orelse
24                     (C >= $A andalso C =< $Z) orelse
25                     (C >= $0 andalso C =< $9) orelse
26                     (C =:= ?FULLSTOP orelse C =:= $- orelse C =:= $~ orelse
27                      C =:= $_))).
28
29hexdigit(C) when C < 10 -> $0 + C;
30hexdigit(C) when C < 16 -> $A + (C - 10).
31
32unhexdigit(C) when C >= $0, C =< $9 -> C - $0;
33unhexdigit(C) when C >= $a, C =< $f -> C - $a + 10;
34unhexdigit(C) when C >= $A, C =< $F -> C - $A + 10.
35
36%% @spec partition(String, Sep) -> {String, [], []} | {Prefix, Sep, Postfix}
37%% @doc Inspired by Python 2.5's str.partition:
38%%      partition("foo/bar", "/") = {"foo", "/", "bar"},
39%%      partition("foo", "/") = {"foo", "", ""}.
40partition(String, Sep) ->
41    case partition(String, Sep, []) of
42        undefined ->
43            {String, "", ""};
44        Result ->
45            Result
46    end.
47
48partition("", _Sep, _Acc) ->
49    undefined;
50partition(S, Sep, Acc) ->
51    case partition2(S, Sep) of
52        undefined ->
53            [C | Rest] = S,
54            partition(Rest, Sep, [C | Acc]);
55        Rest ->
56            {lists:reverse(Acc), Sep, Rest}
57    end.
58
59partition2(Rest, "") ->
60    Rest;
61partition2([C | R1], [C | R2]) ->
62    partition2(R1, R2);
63partition2(_S, _Sep) ->
64    undefined.
65
66
67
68%% @spec safe_relative_path(string()) -> string() | undefined
69%% @doc Return the reduced version of a relative path or undefined if it
70%%      is not safe. safe relative paths can be joined with an absolute path
71%%      and will result in a subdirectory of the absolute path.
72safe_relative_path("/" ++ _) ->
73    undefined;
74safe_relative_path(P) ->
75    safe_relative_path(P, []).
76
77safe_relative_path("", Acc) ->
78    case Acc of
79        [] ->
80            "";
81        _ ->
82            string:join(lists:reverse(Acc), "/")
83    end;
84safe_relative_path(P, Acc) ->
85    case partition(P, "/") of
86        {"", "/", _} ->
87            %% /foo or foo//bar
88            undefined;
89        {"..", _, _} when Acc =:= [] ->
90            undefined;
91        {"..", _, Rest} ->
92            safe_relative_path(Rest, tl(Acc));
93        {Part, "/", ""} ->
94            safe_relative_path("", ["", Part | Acc]);
95        {Part, _, Rest} ->
96            safe_relative_path(Rest, [Part | Acc])
97    end.
98
99%% @spec shell_quote(string()) -> string()
100%% @doc Quote a string according to UNIX shell quoting rules, returns a string
101%%      surrounded by double quotes.
102shell_quote(L) ->
103    shell_quote(L, [$\"]).
104
105%% @spec cmd_port([string()], Options) -> port()
106%% @doc open_port({spawn, mochiweb_util:cmd_string(Argv)}, Options).
107cmd_port(Argv, Options) ->
108    open_port({spawn, cmd_string(Argv)}, Options).
109
110%% @spec cmd([string()]) -> string()
111%% @doc os:cmd(cmd_string(Argv)).
112cmd(Argv) ->
113    os:cmd(cmd_string(Argv)).
114
115%% @spec cmd_string([string()]) -> string()
116%% @doc Create a shell quoted command string from a list of arguments.
117cmd_string(Argv) ->
118    string:join([shell_quote(X) || X <- Argv], " ").
119
120%% @spec cmd_status([string()]) -> {ExitStatus::integer(), Stdout::binary()}
121%% @doc Accumulate the output and exit status from the given application, will be
122%%      spawned with cmd_port/2.
123cmd_status(Argv) ->
124    Port = cmd_port(Argv, [exit_status, stderr_to_stdout,
125                           use_stdio, binary]),
126    try cmd_loop(Port, [])
127    after catch port_close(Port)
128    end.
129
130%% @spec cmd_loop(port(), list()) -> {ExitStatus::integer(), Stdout::binary()}
131%% @doc Accumulate the output and exit status from a port.
132cmd_loop(Port, Acc) ->
133    receive
134        {Port, {exit_status, Status}} ->
135            {Status, iolist_to_binary(lists:reverse(Acc))};
136        {Port, {data, Data}} ->
137            cmd_loop(Port, [Data | Acc])
138    end.
139
140%% @spec join([iolist()], iolist()) -> iolist()
141%% @doc Join a list of strings or binaries together with the given separator
142%%      string or char or binary. The output is flattened, but may be an
143%%      iolist() instead of a string() if any of the inputs are binary().
144join([], _Separator) ->
145    [];
146join([S], _Separator) ->
147    lists:flatten(S);
148join(Strings, Separator) ->
149    lists:flatten(revjoin(lists:reverse(Strings), Separator, [])).
150
151revjoin([], _Separator, Acc) ->
152    Acc;
153revjoin([S | Rest], Separator, []) ->
154    revjoin(Rest, Separator, [S]);
155revjoin([S | Rest], Separator, Acc) ->
156    revjoin(Rest, Separator, [S, Separator | Acc]).
157
158%% @spec quote_plus(atom() | integer() | float() | string() | binary()) -> string()
159%% @doc URL safe encoding of the given term.
160quote_plus(Atom) when is_atom(Atom) ->
161    quote_plus(atom_to_list(Atom));
162quote_plus(Int) when is_integer(Int) ->
163    quote_plus(integer_to_list(Int));
164quote_plus(Binary) when is_binary(Binary) ->
165    quote_plus(binary_to_list(Binary));
166quote_plus(Float) when is_float(Float) ->
167    quote_plus(mochinum:digits(Float));
168quote_plus(String) ->
169    quote_plus(String, []).
170
171quote_plus([], Acc) ->
172    lists:reverse(Acc);
173quote_plus([C | Rest], Acc) when ?QS_SAFE(C) ->
174    quote_plus(Rest, [C | Acc]);
175quote_plus([$\s | Rest], Acc) ->
176    quote_plus(Rest, [$+ | Acc]);
177quote_plus([C | Rest], Acc) ->
178    <<Hi:4, Lo:4>> = <<C>>,
179    quote_plus(Rest, [hexdigit(Lo), hexdigit(Hi), ?PERCENT | Acc]).
180
181%% @spec urlencode([{Key, Value}]) -> string()
182%% @doc URL encode the property list.
183urlencode(Props) ->
184    Pairs = lists:foldr(
185              fun ({K, V}, Acc) ->
186                      [quote_plus(K) ++ "=" ++ quote_plus(V) | Acc]
187              end, [], Props),
188    string:join(Pairs, "&").
189
190%% @spec parse_qs(string() | binary()) -> [{Key, Value}]
191%% @doc Parse a query string or application/x-www-form-urlencoded.
192parse_qs(Binary) when is_binary(Binary) ->
193    parse_qs(binary_to_list(Binary));
194parse_qs(String) ->
195    parse_qs(String, []).
196
197parse_qs([], Acc) ->
198    lists:reverse(Acc);
199parse_qs(String, Acc) ->
200    {Key, Rest} = parse_qs_key(String),
201    {Value, Rest1} = parse_qs_value(Rest),
202    parse_qs(Rest1, [{Key, Value} | Acc]).
203
204parse_qs_key(String) ->
205    parse_qs_key(String, []).
206
207parse_qs_key([], Acc) ->
208    {qs_revdecode(Acc), ""};
209parse_qs_key([$= | Rest], Acc) ->
210    {qs_revdecode(Acc), Rest};
211parse_qs_key(Rest=[$; | _], Acc) ->
212    {qs_revdecode(Acc), Rest};
213parse_qs_key(Rest=[$& | _], Acc) ->
214    {qs_revdecode(Acc), Rest};
215parse_qs_key([C | Rest], Acc) ->
216    parse_qs_key(Rest, [C | Acc]).
217
218parse_qs_value(String) ->
219    parse_qs_value(String, []).
220
221parse_qs_value([], Acc) ->
222    {qs_revdecode(Acc), ""};
223parse_qs_value([$; | Rest], Acc) ->
224    {qs_revdecode(Acc), Rest};
225parse_qs_value([$& | Rest], Acc) ->
226    {qs_revdecode(Acc), Rest};
227parse_qs_value([C | Rest], Acc) ->
228    parse_qs_value(Rest, [C | Acc]).
229
230%% @spec unquote(string() | binary()) -> string()
231%% @doc Unquote a URL encoded string.
232unquote(Binary) when is_binary(Binary) ->
233    unquote(binary_to_list(Binary));
234unquote(String) ->
235    qs_revdecode(lists:reverse(String)).
236
237qs_revdecode(S) ->
238    qs_revdecode(S, []).
239
240qs_revdecode([], Acc) ->
241    Acc;
242qs_revdecode([$+ | Rest], Acc) ->
243    qs_revdecode(Rest, [$\s | Acc]);
244qs_revdecode([Lo, Hi, ?PERCENT | Rest], Acc) when ?IS_HEX(Lo), ?IS_HEX(Hi) ->
245    qs_revdecode(Rest, [(unhexdigit(Lo) bor (unhexdigit(Hi) bsl 4)) | Acc]);
246qs_revdecode([C | Rest], Acc) ->
247    qs_revdecode(Rest, [C | Acc]).
248
249%% @spec urlsplit(Url) -> {Scheme, Netloc, Path, Query, Fragment}
250%% @doc Return a 5-tuple, does not expand % escapes. Only supports HTTP style
251%%      URLs.
252urlsplit(Url) ->
253    {Scheme, Url1} = urlsplit_scheme(Url),
254    {Netloc, Url2} = urlsplit_netloc(Url1),
255    {Path, Query, Fragment} = urlsplit_path(Url2),
256    {Scheme, Netloc, Path, Query, Fragment}.
257
258urlsplit_scheme(Url) ->
259    case urlsplit_scheme(Url, []) of
260        no_scheme ->
261            {"", Url};
262        Res ->
263            Res
264    end.
265
266urlsplit_scheme([C | Rest], Acc) when ((C >= $a andalso C =< $z) orelse
267                                       (C >= $A andalso C =< $Z) orelse
268                                       (C >= $0 andalso C =< $9) orelse
269                                       C =:= $+ orelse C =:= $- orelse
270                                       C =:= $.) ->
271    urlsplit_scheme(Rest, [C | Acc]);
272urlsplit_scheme([$: | Rest], Acc=[_ | _]) ->
273    {string:to_lower(lists:reverse(Acc)), Rest};
274urlsplit_scheme(_Rest, _Acc) ->
275    no_scheme.
276
277urlsplit_netloc("//" ++ Rest) ->
278    urlsplit_netloc(Rest, []);
279urlsplit_netloc(Path) ->
280    {"", Path}.
281
282urlsplit_netloc("", Acc) ->
283    {lists:reverse(Acc), ""};
284urlsplit_netloc(Rest=[C | _], Acc) when C =:= $/; C =:= $?; C =:= $# ->
285    {lists:reverse(Acc), Rest};
286urlsplit_netloc([C | Rest], Acc) ->
287    urlsplit_netloc(Rest, [C | Acc]).
288
289
290%% @spec path_split(string()) -> {Part, Rest}
291%% @doc Split a path starting from the left, as in URL traversal.
292%%      path_split("foo/bar") = {"foo", "bar"},
293%%      path_split("/foo/bar") = {"", "foo/bar"}.
294path_split(S) ->
295    path_split(S, []).
296
297path_split("", Acc) ->
298    {lists:reverse(Acc), ""};
299path_split("/" ++ Rest, Acc) ->
300    {lists:reverse(Acc), Rest};
301path_split([C | Rest], Acc) ->
302    path_split(Rest, [C | Acc]).
303
304
305%% @spec urlunsplit({Scheme, Netloc, Path, Query, Fragment}) -> string()
306%% @doc Assemble a URL from the 5-tuple. Path must be absolute.
307urlunsplit({Scheme, Netloc, Path, Query, Fragment}) ->
308    lists:flatten([case Scheme of "" -> "";  _ -> [Scheme, "://"] end,
309                   Netloc,
310                   urlunsplit_path({Path, Query, Fragment})]).
311
312%% @spec urlunsplit_path({Path, Query, Fragment}) -> string()
313%% @doc Assemble a URL path from the 3-tuple.
314urlunsplit_path({Path, Query, Fragment}) ->
315    lists:flatten([Path,
316                   case Query of "" -> ""; _ -> [$? | Query] end,
317                   case Fragment of "" -> ""; _ -> [$# | Fragment] end]).
318
319%% @spec urlsplit_path(Url) -> {Path, Query, Fragment}
320%% @doc Return a 3-tuple, does not expand % escapes. Only supports HTTP style
321%%      paths.
322urlsplit_path(Path) ->
323    urlsplit_path(Path, []).
324
325urlsplit_path("", Acc) ->
326    {lists:reverse(Acc), "", ""};
327urlsplit_path("?" ++ Rest, Acc) ->
328    {Query, Fragment} = urlsplit_query(Rest),
329    {lists:reverse(Acc), Query, Fragment};
330urlsplit_path("#" ++ Rest, Acc) ->
331    {lists:reverse(Acc), "", Rest};
332urlsplit_path([C | Rest], Acc) ->
333    urlsplit_path(Rest, [C | Acc]).
334
335urlsplit_query(Query) ->
336    urlsplit_query(Query, []).
337
338urlsplit_query("", Acc) ->
339    {lists:reverse(Acc), ""};
340urlsplit_query("#" ++ Rest, Acc) ->
341    {lists:reverse(Acc), Rest};
342urlsplit_query([C | Rest], Acc) ->
343    urlsplit_query(Rest, [C | Acc]).
344
345%% @spec guess_mime(string()) -> string()
346%% @doc  Guess the mime type of a file by the extension of its filename.
347guess_mime(File) ->
348    case mochiweb_mime:from_extension(filename:extension(File)) of
349        undefined ->
350            "text/plain";
351        Mime ->
352            Mime
353    end.
354
355%% @spec parse_header(string()) -> {Type, [{K, V}]}
356%% @doc  Parse a Content-Type like header, return the main Content-Type
357%%       and a property list of options.
358parse_header(String) ->
359    %% TODO: This is exactly as broken as Python's cgi module.
360    %%       Should parse properly like mochiweb_cookies.
361    [Type | Parts] = [string:strip(S) || S <- string:tokens(String, ";")],
362    F = fun (S, Acc) ->
363                case lists:splitwith(fun (C) -> C =/= $= end, S) of
364                    {"", _} ->
365                        %% Skip anything with no name
366                        Acc;
367                    {_, ""} ->
368                        %% Skip anything with no value
369                        Acc;
370                    {Name, [$\= | Value]} ->
371                        [{string:to_lower(string:strip(Name)),
372                          unquote_header(string:strip(Value))} | Acc]
373                end
374        end,
375    {string:to_lower(Type),
376     lists:foldr(F, [], Parts)}.
377
378unquote_header("\"" ++ Rest) ->
379    unquote_header(Rest, []);
380unquote_header(S) ->
381    S.
382
383unquote_header("", Acc) ->
384    lists:reverse(Acc);
385unquote_header("\"", Acc) ->
386    lists:reverse(Acc);
387unquote_header([$\\, C | Rest], Acc) ->
388    unquote_header(Rest, [C | Acc]);
389unquote_header([C | Rest], Acc) ->
390    unquote_header(Rest, [C | Acc]).
391
392%% @spec record_to_proplist(Record, Fields) -> proplist()
393%% @doc calls record_to_proplist/3 with a default TypeKey of '__record'
394record_to_proplist(Record, Fields) ->
395    record_to_proplist(Record, Fields, '__record').
396
397%% @spec record_to_proplist(Record, Fields, TypeKey) -> proplist()
398%% @doc Return a proplist of the given Record with each field in the
399%%      Fields list set as a key with the corresponding value in the Record.
400%%      TypeKey is the key that is used to store the record type
401%%      Fields should be obtained by calling record_info(fields, record_type)
402%%      where record_type is the record type of Record
403record_to_proplist(Record, Fields, TypeKey)
404  when tuple_size(Record) - 1 =:= length(Fields) ->
405    lists:zip([TypeKey | Fields], tuple_to_list(Record)).
406
407
408shell_quote([], Acc) ->
409    lists:reverse([$\" | Acc]);
410shell_quote([C | Rest], Acc) when C =:= $\" orelse C =:= $\` orelse
411                                  C =:= $\\ orelse C =:= $\$ ->
412    shell_quote(Rest, [C, $\\ | Acc]);
413shell_quote([C | Rest], Acc) ->
414    shell_quote(Rest, [C | Acc]).
415
416%% @spec parse_qvalues(string()) -> [qvalue()] | invalid_qvalue_string
417%% @type qvalue() = {media_type() | encoding() , float()}.
418%% @type media_type() = string().
419%% @type encoding() = string().
420%%
421%% @doc Parses a list (given as a string) of elements with Q values associated
422%%      to them. Elements are separated by commas and each element is separated
423%%      from its Q value by a semicolon. Q values are optional but when missing
424%%      the value of an element is considered as 1.0. A Q value is always in the
425%%      range [0.0, 1.0]. A Q value list is used for example as the value of the
426%%      HTTP "Accept" and "Accept-Encoding" headers.
427%%
428%%      Q values are described in section 2.9 of the RFC 2616 (HTTP 1.1).
429%%
430%%      Example:
431%%
432%%      parse_qvalues("gzip; q=0.5, deflate, identity;q=0.0") ->
433%%          [{"gzip", 0.5}, {"deflate", 1.0}, {"identity", 0.0}]
434%%
435parse_qvalues(QValuesStr) ->
436    try
437        lists:map(
438            fun(Pair) ->
439                [Type | Params] = string:tokens(Pair, ";"),
440                NormParams = normalize_media_params(Params),
441                {Q, NonQParams} = extract_q(NormParams),
442                {string:join([string:strip(Type) | NonQParams], ";"), Q}
443            end,
444            string:tokens(string:to_lower(QValuesStr), ",")
445        )
446    catch
447        _Type:_Error ->
448            invalid_qvalue_string
449    end.
450
451normalize_media_params(Params) ->
452    {ok, Re} = re:compile("\\s"),
453    normalize_media_params(Re, Params, []).
454
455normalize_media_params(_Re, [], Acc) ->
456    lists:reverse(Acc);
457normalize_media_params(Re, [Param | Rest], Acc) ->
458    NormParam = re:replace(Param, Re, "", [global, {return, list}]),
459    normalize_media_params(Re, Rest, [NormParam | Acc]).
460
461extract_q(NormParams) ->
462    {ok, KVRe} = re:compile("^([^=]+)=([^=]+)$"),
463    {ok, QRe} = re:compile("^((?:0|1)(?:\\.\\d{1,3})?)$"),
464    extract_q(KVRe, QRe, NormParams, []).
465
466extract_q(_KVRe, _QRe, [], Acc) ->
467    {1.0, lists:reverse(Acc)};
468extract_q(KVRe, QRe, [Param | Rest], Acc) ->
469    case re:run(Param, KVRe, [{capture, [1, 2], list}]) of
470        {match, [Name, Value]} ->
471            case Name of
472            "q" ->
473                {match, [Q]} = re:run(Value, QRe, [{capture, [1], list}]),
474                QVal = case Q of
475                    "0" ->
476                        0.0;
477                    "1" ->
478                        1.0;
479                    Else ->
480                        list_to_float(Else)
481                end,
482                case QVal < 0.0 orelse QVal > 1.0 of
483                false ->
484                    {QVal, lists:reverse(Acc) ++ Rest}
485                end;
486            _ ->
487                extract_q(KVRe, QRe, Rest, [Param | Acc])
488            end
489    end.
490
491%% @spec pick_accepted_encodings([qvalue()], [encoding()], encoding()) ->
492%%    [encoding()]
493%%
494%% @doc Determines which encodings specified in the given Q values list are
495%%      valid according to a list of supported encodings and a default encoding.
496%%
497%%      The returned list of encodings is sorted, descendingly, according to the
498%%      Q values of the given list. The last element of this list is the given
499%%      default encoding unless this encoding is explicitily or implicitily
500%%      marked with a Q value of 0.0 in the given Q values list.
501%%      Note: encodings with the same Q value are kept in the same order as
502%%            found in the input Q values list.
503%%
504%%      This encoding picking process is described in section 14.3 of the
505%%      RFC 2616 (HTTP 1.1).
506%%
507%%      Example:
508%%
509%%      pick_accepted_encodings(
510%%          [{"gzip", 0.5}, {"deflate", 1.0}],
511%%          ["gzip", "identity"],
512%%          "identity"
513%%      ) ->
514%%          ["gzip", "identity"]
515%%
516pick_accepted_encodings(AcceptedEncs, SupportedEncs, DefaultEnc) ->
517    SortedQList = lists:reverse(
518        lists:sort(fun({_, Q1}, {_, Q2}) -> Q1 < Q2 end, AcceptedEncs)
519    ),
520    {Accepted, Refused} = lists:foldr(
521        fun({E, Q}, {A, R}) ->
522            case Q > 0.0 of
523                true ->
524                    {[E | A], R};
525                false ->
526                    {A, [E | R]}
527            end
528        end,
529        {[], []},
530        SortedQList
531    ),
532    Refused1 = lists:foldr(
533        fun(Enc, Acc) ->
534            case Enc of
535                "*" ->
536                    lists:subtract(SupportedEncs, Accepted) ++ Acc;
537                _ ->
538                    [Enc | Acc]
539            end
540        end,
541        [],
542        Refused
543    ),
544    Accepted1 = lists:foldr(
545        fun(Enc, Acc) ->
546            case Enc of
547                "*" ->
548                    lists:subtract(SupportedEncs, Accepted ++ Refused1) ++ Acc;
549                _ ->
550                    [Enc | Acc]
551            end
552        end,
553        [],
554        Accepted
555    ),
556    Accepted2 = case lists:member(DefaultEnc, Accepted1) of
557        true ->
558            Accepted1;
559        false ->
560            Accepted1 ++ [DefaultEnc]
561    end,
562    [E || E <- Accepted2, lists:member(E, SupportedEncs),
563        not lists:member(E, Refused1)].
564
565make_io(Atom) when is_atom(Atom) ->
566    atom_to_list(Atom);
567make_io(Integer) when is_integer(Integer) ->
568    integer_to_list(Integer);
569make_io(Io) when is_list(Io); is_binary(Io) ->
570    Io.
571
572%%
573%% Tests
574%%
575-include_lib("eunit/include/eunit.hrl").
576-ifdef(TEST).
577
578make_io_test() ->
579    ?assertEqual(
580       <<"atom">>,
581       iolist_to_binary(make_io(atom))),
582    ?assertEqual(
583       <<"20">>,
584       iolist_to_binary(make_io(20))),
585    ?assertEqual(
586       <<"list">>,
587       iolist_to_binary(make_io("list"))),
588    ?assertEqual(
589       <<"binary">>,
590       iolist_to_binary(make_io(<<"binary">>))),
591    ok.
592
593-record(test_record, {field1=f1, field2=f2}).
594record_to_proplist_test() ->
595    ?assertEqual(
596       [{'__record', test_record},
597        {field1, f1},
598        {field2, f2}],
599       record_to_proplist(#test_record{}, record_info(fields, test_record))),
600    ?assertEqual(
601       [{'typekey', test_record},
602        {field1, f1},
603        {field2, f2}],
604       record_to_proplist(#test_record{},
605                          record_info(fields, test_record),
606                          typekey)),
607    ok.
608
609shell_quote_test() ->
610    ?assertEqual(
611       "\"foo \\$bar\\\"\\`' baz\"",
612       shell_quote("foo $bar\"`' baz")),
613    ok.
614
615cmd_port_test_spool(Port, Acc) ->
616    receive
617        {Port, eof} ->
618            Acc;
619        {Port, {data, {eol, Data}}} ->
620            cmd_port_test_spool(Port, ["\n", Data | Acc]);
621        {Port, Unknown} ->
622            throw({unknown, Unknown})
623    after 100 ->
624            throw(timeout)
625    end.
626
627cmd_port_test() ->
628    Port = cmd_port(["echo", "$bling$ `word`!"],
629                    [eof, stream, {line, 4096}]),
630    Res = try lists:append(lists:reverse(cmd_port_test_spool(Port, [])))
631          after catch port_close(Port)
632          end,
633    self() ! {Port, wtf},
634    try cmd_port_test_spool(Port, [])
635    catch throw:{unknown, wtf} -> ok
636    end,
637    try cmd_port_test_spool(Port, [])
638    catch throw:timeout -> ok
639    end,
640    ?assertEqual(
641       "$bling$ `word`!\n",
642       Res).
643
644cmd_test() ->
645    ?assertEqual(
646       "$bling$ `word`!\n",
647       cmd(["echo", "$bling$ `word`!"])),
648    ok.
649
650cmd_string_test() ->
651    ?assertEqual(
652       "\"echo\" \"\\$bling\\$ \\`word\\`!\"",
653       cmd_string(["echo", "$bling$ `word`!"])),
654    ok.
655
656cmd_status_test() ->
657    ?assertEqual(
658       {0, <<"$bling$ `word`!\n">>},
659       cmd_status(["echo", "$bling$ `word`!"])),
660    ok.
661
662
663parse_header_test() ->
664    ?assertEqual(
665       {"multipart/form-data", [{"boundary", "AaB03x"}]},
666       parse_header("multipart/form-data; boundary=AaB03x")),
667    %% This tests (currently) intentionally broken behavior
668    ?assertEqual(
669       {"multipart/form-data",
670        [{"b", ""},
671         {"cgi", "is"},
672         {"broken", "true\"e"}]},
673       parse_header("multipart/form-data;b=;cgi=\"i\\s;broken=true\"e;=z;z")),
674    ok.
675
676guess_mime_test() ->
677    "text/plain" = guess_mime(""),
678    "text/plain" = guess_mime(".text"),
679    "application/zip" = guess_mime(".zip"),
680    "application/zip" = guess_mime("x.zip"),
681    "text/html" = guess_mime("x.html"),
682    "application/xhtml+xml" = guess_mime("x.xhtml"),
683    ok.
684
685path_split_test() ->
686    {"", "foo/bar"} = path_split("/foo/bar"),
687    {"foo", "bar"} = path_split("foo/bar"),
688    {"bar", ""} = path_split("bar"),
689    ok.
690
691urlsplit_test() ->
692    {"", "", "/foo", "", "bar?baz"} = urlsplit("/foo#bar?baz"),
693    {"http", "host:port", "/foo", "", "bar?baz"} =
694        urlsplit("http://host:port/foo#bar?baz"),
695    {"http", "host", "", "", ""} = urlsplit("http://host"),
696    {"", "", "/wiki/Category:Fruit", "", ""} =
697        urlsplit("/wiki/Category:Fruit"),
698    ok.
699
700urlsplit_path_test() ->
701    {"/foo/bar", "", ""} = urlsplit_path("/foo/bar"),
702    {"/foo", "baz", ""} = urlsplit_path("/foo?baz"),
703    {"/foo", "", "bar?baz"} = urlsplit_path("/foo#bar?baz"),
704    {"/foo", "", "bar?baz#wibble"} = urlsplit_path("/foo#bar?baz#wibble"),
705    {"/foo", "bar", "baz"} = urlsplit_path("/foo?bar#baz"),
706    {"/foo", "bar?baz", "baz"} = urlsplit_path("/foo?bar?baz#baz"),
707    ok.
708
709urlunsplit_test() ->
710    "/foo#bar?baz" = urlunsplit({"", "", "/foo", "", "bar?baz"}),
711    "http://host:port/foo#bar?baz" =
712        urlunsplit({"http", "host:port", "/foo", "", "bar?baz"}),
713    ok.
714
715urlunsplit_path_test() ->
716    "/foo/bar" = urlunsplit_path({"/foo/bar", "", ""}),
717    "/foo?baz" = urlunsplit_path({"/foo", "baz", ""}),
718    "/foo#bar?baz" = urlunsplit_path({"/foo", "", "bar?baz"}),
719    "/foo#bar?baz#wibble" = urlunsplit_path({"/foo", "", "bar?baz#wibble"}),
720    "/foo?bar#baz" = urlunsplit_path({"/foo", "bar", "baz"}),
721    "/foo?bar?baz#baz" = urlunsplit_path({"/foo", "bar?baz", "baz"}),
722    ok.
723
724join_test() ->
725    ?assertEqual("foo,bar,baz",
726                  join(["foo", "bar", "baz"], $,)),
727    ?assertEqual("foo,bar,baz",
728                  join(["foo", "bar", "baz"], ",")),
729    ?assertEqual("foo bar",
730                  join([["foo", " bar"]], ",")),
731    ?assertEqual("foo bar,baz",
732                  join([["foo", " bar"], "baz"], ",")),
733    ?assertEqual("foo",
734                  join(["foo"], ",")),
735    ?assertEqual("foobarbaz",
736                  join(["foo", "bar", "baz"], "")),
737    ?assertEqual("foo" ++ [<<>>] ++ "bar" ++ [<<>>] ++ "baz",
738                 join(["foo", "bar", "baz"], <<>>)),
739    ?assertEqual("foobar" ++ [<<"baz">>],
740                 join(["foo", "bar", <<"baz">>], "")),
741    ?assertEqual("",
742                 join([], "any")),
743    ok.
744
745quote_plus_test() ->
746    "foo" = quote_plus(foo),
747    "1" = quote_plus(1),
748    "1.1" = quote_plus(1.1),
749    "foo" = quote_plus("foo"),
750    "foo+bar" = quote_plus("foo bar"),
751    "foo%0A" = quote_plus("foo\n"),
752    "foo%0A" = quote_plus("foo\n"),
753    "foo%3B%26%3D" = quote_plus("foo;&="),
754    "foo%3B%26%3D" = quote_plus(<<"foo;&=">>),
755    ok.
756
757unquote_test() ->
758    ?assertEqual("foo bar",
759                 unquote("foo+bar")),
760    ?assertEqual("foo bar",
761                 unquote("foo%20bar")),
762    ?assertEqual("foo\r\n",
763                 unquote("foo%0D%0A")),
764    ?assertEqual("foo\r\n",
765                 unquote(<<"foo%0D%0A">>)),
766    ok.
767
768urlencode_test() ->
769    "foo=bar&baz=wibble+%0D%0A&z=1" = urlencode([{foo, "bar"},
770                                                 {"baz", "wibble \r\n"},
771                                                 {z, 1}]),
772    ok.
773
774parse_qs_test() ->
775    ?assertEqual(
776       [{"foo", "bar"}, {"baz", "wibble \r\n"}, {"z", "1"}],
777       parse_qs("foo=bar&baz=wibble+%0D%0a&z=1")),
778    ?assertEqual(
779       [{"", "bar"}, {"baz", "wibble \r\n"}, {"z", ""}],
780       parse_qs("=bar&baz=wibble+%0D%0a&z=")),
781    ?assertEqual(
782       [{"foo", "bar"}, {"baz", "wibble \r\n"}, {"z", "1"}],
783       parse_qs(<<"foo=bar&baz=wibble+%0D%0a&z=1">>)),
784    ?assertEqual(
785       [],
786       parse_qs("")),
787    ?assertEqual(
788       [{"foo", ""}, {"bar", ""}, {"baz", ""}],
789       parse_qs("foo;bar&baz")),
790    ok.
791
792partition_test() ->
793    {"foo", "", ""} = partition("foo", "/"),
794    {"foo", "/", "bar"} = partition("foo/bar", "/"),
795    {"foo", "/", ""} = partition("foo/", "/"),
796    {"", "/", "bar"} = partition("/bar", "/"),
797    {"f", "oo/ba", "r"} = partition("foo/bar", "oo/ba"),
798    ok.
799
800safe_relative_path_test() ->
801    "foo" = safe_relative_path("foo"),
802    "foo/" = safe_relative_path("foo/"),
803    "foo" = safe_relative_path("foo/bar/.."),
804    "bar" = safe_relative_path("foo/../bar"),
805    "bar/" = safe_relative_path("foo/../bar/"),
806    "" = safe_relative_path("foo/.."),
807    "" = safe_relative_path("foo/../"),
808    undefined = safe_relative_path("/foo"),
809    undefined = safe_relative_path("../foo"),
810    undefined = safe_relative_path("foo/../.."),
811    undefined = safe_relative_path("foo//"),
812    ok.
813
814parse_qvalues_test() ->
815    [] = parse_qvalues(""),
816    [{"identity", 0.0}] = parse_qvalues("identity;q=0"),
817    [{"identity", 0.0}] = parse_qvalues("identity ;q=0"),
818    [{"identity", 0.0}] = parse_qvalues(" identity; q =0 "),
819    [{"identity", 0.0}] = parse_qvalues("identity ; q = 0"),
820    [{"identity", 0.0}] = parse_qvalues("identity ; q= 0.0"),
821    [{"gzip", 1.0}, {"deflate", 1.0}, {"identity", 0.0}] = parse_qvalues(
822        "gzip,deflate,identity;q=0.0"
823    ),
824    [{"deflate", 1.0}, {"gzip", 1.0}, {"identity", 0.0}] = parse_qvalues(
825        "deflate,gzip,identity;q=0.0"
826    ),
827    [{"gzip", 1.0}, {"deflate", 1.0}, {"gzip", 1.0}, {"identity", 0.0}] =
828        parse_qvalues("gzip,deflate,gzip,identity;q=0"),
829    [{"gzip", 1.0}, {"deflate", 1.0}, {"identity", 0.0}] = parse_qvalues(
830        "gzip, deflate , identity; q=0.0"
831    ),
832    [{"gzip", 1.0}, {"deflate", 1.0}, {"identity", 0.0}] = parse_qvalues(
833        "gzip; q=1, deflate;q=1.0, identity;q=0.0"
834    ),
835    [{"gzip", 0.5}, {"deflate", 1.0}, {"identity", 0.0}] = parse_qvalues(
836        "gzip; q=0.5, deflate;q=1.0, identity;q=0"
837    ),
838    [{"gzip", 0.5}, {"deflate", 1.0}, {"identity", 0.0}] = parse_qvalues(
839        "gzip; q=0.5, deflate , identity;q=0.0"
840    ),
841    [{"gzip", 0.5}, {"deflate", 0.8}, {"identity", 0.0}] = parse_qvalues(
842        "gzip; q=0.5, deflate;q=0.8, identity;q=0.0"
843    ),
844    [{"gzip", 0.5}, {"deflate", 1.0}, {"identity", 1.0}] = parse_qvalues(
845        "gzip; q=0.5,deflate,identity"
846    ),
847    [{"gzip", 0.5}, {"deflate", 1.0}, {"identity", 1.0}, {"identity", 1.0}] =
848        parse_qvalues("gzip; q=0.5,deflate,identity, identity "),
849    [{"text/html;level=1", 1.0}, {"text/plain", 0.5}] =
850        parse_qvalues("text/html;level=1, text/plain;q=0.5"),
851    [{"text/html;level=1", 0.3}, {"text/plain", 1.0}] =
852        parse_qvalues("text/html;level=1;q=0.3, text/plain"),
853    [{"text/html;level=1", 0.3}, {"text/plain", 1.0}] =
854        parse_qvalues("text/html; level = 1; q = 0.3, text/plain"),
855    [{"text/html;level=1", 0.3}, {"text/plain", 1.0}] =
856        parse_qvalues("text/html;q=0.3;level=1, text/plain"),
857    invalid_qvalue_string = parse_qvalues("gzip; q=1.1, deflate"),
858    invalid_qvalue_string = parse_qvalues("gzip; q=0.5, deflate;q=2"),
859    invalid_qvalue_string = parse_qvalues("gzip, deflate;q=AB"),
860    invalid_qvalue_string = parse_qvalues("gzip; q=2.1, deflate"),
861    invalid_qvalue_string = parse_qvalues("gzip; q=0.1234, deflate"),
862    invalid_qvalue_string = parse_qvalues("text/html;level=1;q=0.3, text/html;level"),
863    ok.
864
865pick_accepted_encodings_test() ->
866    ["identity"] = pick_accepted_encodings(
867        [],
868        ["gzip", "identity"],
869        "identity"
870    ),
871    ["gzip", "identity"] = pick_accepted_encodings(
872        [{"gzip", 1.0}],
873        ["gzip", "identity"],
874        "identity"
875    ),
876    ["identity"] = pick_accepted_encodings(
877        [{"gzip", 0.0}],
878        ["gzip", "identity"],
879        "identity"
880    ),
881    ["gzip", "identity"] = pick_accepted_encodings(
882        [{"gzip", 1.0}, {"deflate", 1.0}],
883        ["gzip", "identity"],
884        "identity"
885    ),
886    ["gzip", "identity"] = pick_accepted_encodings(
887        [{"gzip", 0.5}, {"deflate", 1.0}],
888        ["gzip", "identity"],
889        "identity"
890    ),
891    ["identity"] = pick_accepted_encodings(
892        [{"gzip", 0.0}, {"deflate", 0.0}],
893        ["gzip", "identity"],
894        "identity"
895    ),
896    ["gzip"] = pick_accepted_encodings(
897        [{"gzip", 1.0}, {"deflate", 1.0}, {"identity", 0.0}],
898        ["gzip", "identity"],
899        "identity"
900    ),
901    ["gzip", "deflate", "identity"] = pick_accepted_encodings(
902        [{"gzip", 1.0}, {"deflate", 1.0}],
903        ["gzip", "deflate", "identity"],
904        "identity"
905    ),
906    ["gzip", "deflate"] = pick_accepted_encodings(
907        [{"gzip", 1.0}, {"deflate", 1.0}, {"identity", 0.0}],
908        ["gzip", "deflate", "identity"],
909        "identity"
910    ),
911    ["deflate", "gzip", "identity"] = pick_accepted_encodings(
912        [{"gzip", 0.2}, {"deflate", 1.0}],
913        ["gzip", "deflate", "identity"],
914        "identity"
915    ),
916    ["deflate", "deflate", "gzip", "identity"] = pick_accepted_encodings(
917        [{"gzip", 0.2}, {"deflate", 1.0}, {"deflate", 1.0}],
918        ["gzip", "deflate", "identity"],
919        "identity"
920    ),
921    ["deflate", "gzip", "gzip", "identity"] = pick_accepted_encodings(
922        [{"gzip", 0.2}, {"deflate", 1.0}, {"gzip", 1.0}],
923        ["gzip", "deflate", "identity"],
924        "identity"
925    ),
926    ["gzip", "deflate", "gzip", "identity"] = pick_accepted_encodings(
927        [{"gzip", 0.2}, {"deflate", 0.9}, {"gzip", 1.0}],
928        ["gzip", "deflate", "identity"],
929        "identity"
930    ),
931    [] = pick_accepted_encodings(
932        [{"*", 0.0}],
933        ["gzip", "deflate", "identity"],
934        "identity"
935    ),
936    ["gzip", "deflate", "identity"] = pick_accepted_encodings(
937        [{"*", 1.0}],
938        ["gzip", "deflate", "identity"],
939        "identity"
940    ),
941    ["gzip", "deflate", "identity"] = pick_accepted_encodings(
942        [{"*", 0.6}],
943        ["gzip", "deflate", "identity"],
944        "identity"
945    ),
946    ["gzip"] = pick_accepted_encodings(
947        [{"gzip", 1.0}, {"*", 0.0}],
948        ["gzip", "deflate", "identity"],
949        "identity"
950    ),
951    ["gzip", "deflate"] = pick_accepted_encodings(
952        [{"gzip", 1.0}, {"deflate", 0.6}, {"*", 0.0}],
953        ["gzip", "deflate", "identity"],
954        "identity"
955    ),
956    ["deflate", "gzip"] = pick_accepted_encodings(
957        [{"gzip", 0.5}, {"deflate", 1.0}, {"*", 0.0}],
958        ["gzip", "deflate", "identity"],
959        "identity"
960    ),
961    ["gzip", "identity"] = pick_accepted_encodings(
962        [{"deflate", 0.0}, {"*", 1.0}],
963        ["gzip", "deflate", "identity"],
964        "identity"
965    ),
966    ["gzip", "identity"] = pick_accepted_encodings(
967        [{"*", 1.0}, {"deflate", 0.0}],
968        ["gzip", "deflate", "identity"],
969        "identity"
970    ),
971    ok.
972
973-endif.
974