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