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