PageRenderTime 42ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/src/mochiweb_util.erl

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