源战役
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

1048 wiersze
35 KiB

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