diff options
author | Dave Cottlehuber <dch@apache.org> | 2013-03-12 20:11:08 +0100 |
---|---|---|
committer | Dave Cottlehuber <dch@apache.org> | 2013-04-25 00:13:20 +0200 |
commit | cbb8a55082a19897e8d1350db5351bf41689185d (patch) | |
tree | a38fe3c099390b5dcfe90798bcd79820606af536 | |
parent | ae6f1ebd8d0c63384050eb8c83b401a01095ad2c (diff) | |
download | couchdb-cbb8a55082a19897e8d1350db5351bf41689185d.tar.gz |
COUCHDB-1696 import mochiweb from tag v2.4.2
32 files changed, 3460 insertions, 805 deletions
diff --git a/src/mochiweb/mochifmt.erl b/src/mochiweb/mochifmt.erl index 5bc6b9c4f..fc95e4f60 100644 --- a/src/mochiweb/mochifmt.erl +++ b/src/mochiweb/mochifmt.erl @@ -369,8 +369,8 @@ parse_std_conversion([Type], Acc) -> %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). tokenize_test() -> {?MODULE, [{raw, "ABC"}]} = tokenize("ABC"), diff --git a/src/mochiweb/mochifmt_records.erl b/src/mochiweb/mochifmt_records.erl index 2326d1dda..7d166ffdc 100644 --- a/src/mochiweb/mochifmt_records.erl +++ b/src/mochiweb/mochifmt_records.erl @@ -9,11 +9,15 @@ %% M:format("{0.bar}", [#rec{bar=foo}]). %% foo --module(mochifmt_records, [Recs]). +-module(mochifmt_records). -author('bob@mochimedia.com'). --export([get_value/2]). +-export([new/1, get_value/3]). -get_value(Key, Rec) when is_tuple(Rec) and is_atom(element(1, Rec)) -> +new([{_Rec, RecFields}]=Recs) when is_list(RecFields) -> + {?MODULE, Recs}. + +get_value(Key, Rec, {?MODULE, Recs}) + when is_tuple(Rec) and is_atom(element(1, Rec)) -> try begin Atom = list_to_existing_atom(Key), {_, Fields} = proplists:lookup(element(1, Rec), Recs), @@ -21,7 +25,7 @@ get_value(Key, Rec) when is_tuple(Rec) and is_atom(element(1, Rec)) -> end catch error:_ -> mochifmt:get_value(Key, Rec) end; -get_value(Key, Args) -> +get_value(Key, Args, {?MODULE, _Recs}) -> mochifmt:get_value(Key, Args). get_rec_index(Atom, [Atom | _], Index) -> @@ -33,6 +37,6 @@ get_rec_index(Atom, [_ | Rest], Index) -> %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). -endif. diff --git a/src/mochiweb/mochifmt_std.erl b/src/mochiweb/mochifmt_std.erl index d4d74f6f6..ea68c4ae5 100644 --- a/src/mochiweb/mochifmt_std.erl +++ b/src/mochiweb/mochifmt_std.erl @@ -3,28 +3,31 @@ %% @doc Template module for a mochifmt formatter. --module(mochifmt_std, []). +-module(mochifmt_std). -author('bob@mochimedia.com'). --export([format/2, get_value/2, format_field/2, get_field/2, convert_field/2]). +-export([new/0, format/3, get_value/3, format_field/3, get_field/3, convert_field/3]). -format(Format, Args) -> +new() -> + {?MODULE}. + +format(Format, Args, {?MODULE}=THIS) -> mochifmt:format(Format, Args, THIS). -get_field(Key, Args) -> +get_field(Key, Args, {?MODULE}=THIS) -> mochifmt:get_field(Key, Args, THIS). -convert_field(Key, Args) -> +convert_field(Key, Args, {?MODULE}) -> mochifmt:convert_field(Key, Args). -get_value(Key, Args) -> +get_value(Key, Args, {?MODULE}) -> mochifmt:get_value(Key, Args). -format_field(Arg, Format) -> +format_field(Arg, Format, {?MODULE}=THIS) -> mochifmt:format_field(Arg, Format, THIS). %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). -endif. diff --git a/src/mochiweb/mochiglobal.erl b/src/mochiweb/mochiglobal.erl index c740b8781..ea645b061 100644 --- a/src/mochiweb/mochiglobal.erl +++ b/src/mochiweb/mochiglobal.erl @@ -30,7 +30,7 @@ put(K, V) -> put(_K, V, Mod) -> Bin = compile(Mod, V), code:purge(Mod), - code:load_binary(Mod, atom_to_list(Mod) ++ ".erl", Bin), + {module, Mod} = code:load_binary(Mod, atom_to_list(Mod) ++ ".erl", Bin), ok. -spec delete(atom()) -> boolean(). @@ -77,8 +77,8 @@ term_to_abstract(Module, Getter, T) -> %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). get_put_delete_test() -> K = '$$test$$mochiglobal', delete(K), diff --git a/src/mochiweb/mochihex.erl b/src/mochiweb/mochihex.erl index 44a2aa7fa..796f3ada2 100644 --- a/src/mochiweb/mochihex.erl +++ b/src/mochiweb/mochihex.erl @@ -8,9 +8,6 @@ -export([to_hex/1, to_bin/1, to_int/1, dehex/1, hexdigit/1]). -%% @type iolist() = [char() | binary() | iolist()] -%% @type iodata() = iolist() | binary() - %% @spec to_hex(integer | iolist()) -> string() %% @doc Convert an iolist to a hexadecimal string. to_hex(0) -> @@ -68,8 +65,8 @@ to_bin([C1, C2 | Rest], Acc) -> %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). to_hex_test() -> "ff000ff1" = to_hex([255, 0, 15, 241]), diff --git a/src/mochiweb/mochijson.erl b/src/mochiweb/mochijson.erl index 2e3d1452c..d28318993 100644 --- a/src/mochiweb/mochijson.erl +++ b/src/mochiweb/mochijson.erl @@ -15,8 +15,6 @@ -define(INC_COL(S), S#decoder{column=1+S#decoder.column}). -define(INC_LINE(S), S#decoder{column=1, line=1+S#decoder.line}). -%% @type iolist() = [char() | binary() | iolist()] -%% @type iodata() = iolist() | binary() %% @type json_string() = atom | string() | binary() %% @type json_number() = integer() | float() %% @type json_array() = {array, [json_term()]} @@ -406,8 +404,8 @@ tokenize(L=[C | _], S) when C >= $0, C =< $9; C == $- -> %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). %% testing constructs borrowed from the Yaws JSON implementation. diff --git a/src/mochiweb/mochijson2.erl b/src/mochiweb/mochijson2.erl index bdf6d77c8..2b8d16e46 100644 --- a/src/mochiweb/mochijson2.erl +++ b/src/mochiweb/mochijson2.erl @@ -40,9 +40,9 @@ -module(mochijson2). -author('bob@mochimedia.com'). -export([encoder/1, encode/1]). --export([decoder/1, decode/1]). +-export([decoder/1, decode/1, decode/2]). -% This is a macro to placate syntax highlighters.. +%% This is a macro to placate syntax highlighters.. -define(Q, $\"). -define(ADV_COL(S, N), S#decoder{offset=N+S#decoder.offset, column=N+S#decoder.column}). @@ -64,15 +64,14 @@ -define(IS_WHITESPACE(C), (C =:= $\s orelse C =:= $\t orelse C =:= $\r orelse C =:= $\n)). -%% @type iolist() = [char() | binary() | iolist()] -%% @type iodata() = iolist() | binary() %% @type json_string() = atom | binary() %% @type json_number() = integer() | float() %% @type json_array() = [json_term()] %% @type json_object() = {struct, [{json_string(), json_term()}]} +%% @type json_eep18_object() = {[{json_string(), json_term()}]} %% @type json_iolist() = {json, iolist()} %% @type json_term() = json_string() | json_number() | json_array() | -%% json_object() | json_iolist() +%% json_object() | json_eep18_object() | json_iolist() -record(encoder, {handler=null, utf8=false}). @@ -102,6 +101,14 @@ decoder(Options) -> State = parse_decoder_options(Options, #decoder{}), fun (O) -> json_decode(O, State) end. +%% @spec decode(iolist(), [{format, proplist | eep18 | struct}]) -> json_term() +%% @doc Decode the given iolist to Erlang terms using the given object format +%% for decoding, where proplist returns JSON objects as [{binary(), json_term()}] +%% proplists, eep18 returns JSON objects as {[binary(), json_term()]}, and struct +%% returns them as-is. +decode(S, Options) -> + json_decode(S, parse_decoder_options(Options, #decoder{})). + %% @spec decode(iolist()) -> json_term() %% @doc Decode the given iolist to Erlang terms. decode(S) -> @@ -119,7 +126,10 @@ parse_encoder_options([{utf8, Switch} | Rest], State) -> parse_decoder_options([], State) -> State; parse_decoder_options([{object_hook, Hook} | Rest], State) -> - parse_decoder_options(Rest, State#decoder{object_hook=Hook}). + parse_decoder_options(Rest, State#decoder{object_hook=Hook}); +parse_decoder_options([{format, Format} | Rest], State) + when Format =:= struct orelse Format =:= eep18 orelse Format =:= proplist -> + parse_decoder_options(Rest, State#decoder{object_hook=Format}). json_encode(true, _State) -> <<"true">>; @@ -139,6 +149,10 @@ json_encode([{K, _}|_] = Props, State) when (K =/= struct andalso json_encode_proplist(Props, State); json_encode({struct, Props}, State) when is_list(Props) -> json_encode_proplist(Props, State); +json_encode({Props}, State) when is_list(Props) -> + json_encode_proplist(Props, State); +json_encode({}, State) -> + json_encode_proplist([], State); json_encode(Array, State) when is_list(Array) -> json_encode_array(Array, State); json_encode({array, Array}, State) when is_list(Array) -> @@ -318,8 +332,12 @@ decode1(B, S=#decoder{state=null}) -> decode_object(B, S1) end. -make_object(V, #decoder{object_hook=null}) -> +make_object(V, #decoder{object_hook=N}) when N =:= null orelse N =:= struct -> V; +make_object({struct, P}, #decoder{object_hook=eep18}) -> + {P}; +make_object({struct, P}, #decoder{object_hook=proplist}) -> + P; make_object(V, #decoder{object_hook=Hook}) -> Hook(V). @@ -559,8 +577,8 @@ tokenize(B, S=#decoder{offset=O}) -> %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). %% testing constructs borrowed from the Yaws JSON implementation. @@ -838,12 +856,34 @@ float_test() -> handler_test() -> ?assertEqual( - {'EXIT',{json_encode,{bad_term,{}}}}, - catch encode({})), - F = fun ({}) -> [] end, + {'EXIT',{json_encode,{bad_term,{x,y}}}}, + catch encode({x,y})), + F = fun ({x,y}) -> [] end, ?assertEqual( <<"[]">>, - iolist_to_binary((encoder([{handler, F}]))({}))), + iolist_to_binary((encoder([{handler, F}]))({x, y}))), ok. +encode_empty_test_() -> + [{A, ?_assertEqual(<<"{}">>, iolist_to_binary(encode(B)))} + || {A, B} <- [{"eep18 {}", {}}, + {"eep18 {[]}", {[]}}, + {"{struct, []}", {struct, []}}]]. + +encode_test_() -> + P = [{<<"k">>, <<"v">>}], + JSON = iolist_to_binary(encode({struct, P})), + [{atom_to_list(F), + ?_assertEqual(JSON, iolist_to_binary(encode(decode(JSON, [{format, F}]))))} + || F <- [struct, eep18, proplist]]. + +format_test_() -> + P = [{<<"k">>, <<"v">>}], + JSON = iolist_to_binary(encode({struct, P})), + [{atom_to_list(F), + ?_assertEqual(A, decode(JSON, [{format, F}]))} + || {F, A} <- [{struct, {struct, P}}, + {eep18, {P}}, + {proplist, P}]]. + -endif. diff --git a/src/mochiweb/mochilists.erl b/src/mochiweb/mochilists.erl index 8981e7b62..d93b241fb 100644 --- a/src/mochiweb/mochilists.erl +++ b/src/mochiweb/mochilists.erl @@ -55,8 +55,8 @@ get_value(Key, Proplist, Default) -> %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). set_defaults_test() -> ?assertEqual( diff --git a/src/mochiweb/mochilogfile2.erl b/src/mochiweb/mochilogfile2.erl index c34ee73ad..b4a7e3c62 100644 --- a/src/mochiweb/mochilogfile2.erl +++ b/src/mochiweb/mochilogfile2.erl @@ -57,8 +57,8 @@ find_last_newline(FD, Location) -> %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). name_test() -> D = mochitemp:mkdtemp(), FileName = filename:join(D, "open_close_test.log"), diff --git a/src/mochiweb/mochinum.erl b/src/mochiweb/mochinum.erl index 3c96b13a2..c52b15ca5 100644 --- a/src/mochiweb/mochinum.erl +++ b/src/mochiweb/mochinum.erl @@ -243,8 +243,8 @@ frexp_int(F) -> %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). int_ceil_test() -> ?assertEqual(1, int_ceil(0.0001)), diff --git a/src/mochiweb/mochitemp.erl b/src/mochiweb/mochitemp.erl index bb23d2a60..dda78632d 100644 --- a/src/mochiweb/mochitemp.erl +++ b/src/mochiweb/mochitemp.erl @@ -135,8 +135,9 @@ normalize_dir(L) -> %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + pushenv(L) -> [{K, os:getenv(K)} || K <- L]. popenv(L) -> diff --git a/src/mochiweb/mochiutf8.erl b/src/mochiweb/mochiutf8.erl index 206e11867..28f28c1ba 100644 --- a/src/mochiweb/mochiutf8.erl +++ b/src/mochiweb/mochiutf8.erl @@ -5,8 +5,9 @@ %% invalid bytes. -module(mochiutf8). --export([valid_utf8_bytes/1, codepoint_to_bytes/1, bytes_to_codepoints/1]). --export([bytes_foldl/3, codepoint_foldl/3, read_codepoint/1, len/1]). +-export([valid_utf8_bytes/1, codepoint_to_bytes/1, codepoints_to_bytes/1]). +-export([bytes_to_codepoints/1, bytes_foldl/3, codepoint_foldl/3]). +-export([read_codepoint/1, len/1]). %% External API @@ -192,8 +193,8 @@ invalid_utf8_indexes(<<>>, _N, Acc) -> %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). binary_skip_bytes_test() -> ?assertEqual(<<"foo">>, diff --git a/src/mochiweb/mochiweb.app.src b/src/mochiweb/mochiweb.app.src index 37a21fbf0..baced90ef 100644 --- a/src/mochiweb/mochiweb.app.src +++ b/src/mochiweb/mochiweb.app.src @@ -1,9 +1,9 @@ %% This is generated from src/mochiweb.app.src {application, mochiweb, [{description, "MochiMedia Web Server"}, - {vsn, "1.4.1"}, + {vsn, "2.4.2"}, {modules, []}, {registered, []}, - {mod, {mochiweb_app, []}}, {env, []}, - {applications, [kernel, stdlib, crypto, inets]}]}. + {applications, [kernel, stdlib, crypto, inets, ssl, xmerl, + compiler, syntax_tools]}]}. diff --git a/src/mochiweb/mochiweb.erl b/src/mochiweb/mochiweb.erl index 3118028b1..250beb5ce 100644 --- a/src/mochiweb/mochiweb.erl +++ b/src/mochiweb/mochiweb.erl @@ -6,22 +6,9 @@ -module(mochiweb). -author('bob@mochimedia.com'). --export([start/0, stop/0]). -export([new_request/1, new_response/1]). -export([all_loaded/0, all_loaded/1, reload/0]). - -%% @spec start() -> ok -%% @doc Start the MochiWeb server. -start() -> - ensure_started(crypto), - application:start(mochiweb). - -%% @spec stop() -> ok -%% @doc Stop the MochiWeb server. -stop() -> - Res = application:stop(mochiweb), - application:stop(crypto), - Res. +-export([ensure_started/1]). reload() -> [c:l(Module) || Module <- all_loaded()]. @@ -78,8 +65,8 @@ new_response({Request, Code, Headers}) -> Code, mochiweb_headers:make(Headers)). -%% Internal API - +%% @spec ensure_started(App::atom()) -> ok +%% @doc Start the given App if it has not been started already. ensure_started(App) -> case application:start(App) of ok -> @@ -88,12 +75,11 @@ ensure_started(App) -> ok end. - %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). -record(treq, {path, body= <<>>, xreply= <<>>}). @@ -112,7 +98,7 @@ with_server(Transport, ServerFun, ClientFun) -> ssl -> ServerOpts0 ++ [{ssl, true}, {ssl_opts, ssl_cert_opts()}] end, - {ok, Server} = mochiweb_http:start(ServerOpts), + {ok, Server} = mochiweb_http:start_link(ServerOpts), Port = mochiweb_socket_server:get(Server, port), Res = (catch ClientFun(Transport, Port)), mochiweb_http:stop(Server), @@ -123,6 +109,8 @@ request_test() -> "/foo/bar/baz wibble quux" = R:get(path), ok. +-define(LARGE_TIMEOUT, 60). + single_http_GET_test() -> do_GET(plain, 1). @@ -135,11 +123,13 @@ multiple_http_GET_test() -> multiple_https_GET_test() -> do_GET(ssl, 3). -hundred_http_GET_test() -> - do_GET(plain, 100). +hundred_http_GET_test_() -> % note the underscore + {timeout, ?LARGE_TIMEOUT, + fun() -> ?assertEqual(ok, do_GET(plain,100)) end}. -hundred_https_GET_test() -> - do_GET(ssl, 100). +hundred_https_GET_test_() -> % note the underscore + {timeout, ?LARGE_TIMEOUT, + fun() -> ?assertEqual(ok, do_GET(ssl,100)) end}. single_128_http_POST_test() -> do_POST(plain, 128, 1). @@ -165,11 +155,13 @@ multiple_100k_http_POST_test() -> multiple_100K_https_POST_test() -> do_POST(ssl, 102400, 3). -hundred_128_http_POST_test() -> - do_POST(plain, 128, 100). +hundred_128_http_POST_test_() -> % note the underscore + {timeout, ?LARGE_TIMEOUT, + fun() -> ?assertEqual(ok, do_POST(plain, 128, 100)) end}. -hundred_128_https_POST_test() -> - do_POST(ssl, 128, 100). +hundred_128_https_POST_test_() -> % note the underscore + {timeout, ?LARGE_TIMEOUT, + fun() -> ?assertEqual(ok, do_POST(ssl, 128, 100)) end}. do_GET(Transport, Times) -> PathPrefix = "/whatever/", diff --git a/src/mochiweb/mochiweb_acceptor.erl b/src/mochiweb/mochiweb_acceptor.erl index 893f99b11..efedfbdc5 100644 --- a/src/mochiweb/mochiweb_acceptor.erl +++ b/src/mochiweb/mochiweb_acceptor.erl @@ -18,14 +18,13 @@ init(Server, Listen, Loop) -> case catch mochiweb_socket:accept(Listen) of {ok, Socket} -> gen_server:cast(Server, {accepted, self(), timer:now_diff(now(), T1)}), - case mochiweb_socket:after_accept(Socket) of - ok -> call_loop(Loop, Socket); - {error, _} -> exit(normal) - end; + call_loop(Loop, Socket); {error, closed} -> exit(normal); {error, timeout} -> init(Server, Listen, Loop); + {error, esslaccept} -> + exit(normal); Other -> error_logger:error_report( [{application, mochiweb}, @@ -36,6 +35,8 @@ init(Server, Listen, Loop) -> call_loop({M, F}, Socket) -> M:F(Socket); +call_loop({M, F, [A1]}, Socket) -> + M:F(Socket, A1); call_loop({M, F, A}, Socket) -> erlang:apply(M, F, [Socket | A]); call_loop(Loop, Socket) -> @@ -44,6 +45,6 @@ call_loop(Loop, Socket) -> %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). -endif. diff --git a/src/mochiweb/mochiweb_charref.erl b/src/mochiweb/mochiweb_charref.erl index 99cd55028..193c7c7c3 100644 --- a/src/mochiweb/mochiweb_charref.erl +++ b/src/mochiweb/mochiweb_charref.erl @@ -1,17 +1,17 @@ %% @author Bob Ippolito <bob@mochimedia.com> %% @copyright 2007 Mochi Media, Inc. -%% @doc Converts HTML 4 charrefs and entities to codepoints. +%% @doc Converts HTML 5 charrefs and entities to codepoints (or lists of code points). -module(mochiweb_charref). -export([charref/1]). %% External API. -%% @spec charref(S) -> integer() | undefined %% @doc Convert a decimal charref, hex charref, or html entity to a unicode %% codepoint, or return undefined on failure. %% The input should not include an ampersand or semicolon. %% charref("#38") = 38, charref("#x26") = 38, charref("amp") = 38. +-spec charref(binary() | string()) -> integer() | [integer()] | undefined. charref(B) when is_binary(B) -> charref(binary_to_list(B)); charref([$#, C | L]) when C =:= $x orelse C =:= $X -> @@ -29,266 +29,2141 @@ charref(L) -> %% Internal API. -entity("nbsp") -> 160; -entity("iexcl") -> 161; -entity("cent") -> 162; -entity("pound") -> 163; -entity("curren") -> 164; -entity("yen") -> 165; -entity("brvbar") -> 166; -entity("sect") -> 167; -entity("uml") -> 168; -entity("copy") -> 169; -entity("ordf") -> 170; -entity("laquo") -> 171; -entity("not") -> 172; -entity("shy") -> 173; -entity("reg") -> 174; -entity("macr") -> 175; -entity("deg") -> 176; -entity("plusmn") -> 177; -entity("sup2") -> 178; -entity("sup3") -> 179; -entity("acute") -> 180; -entity("micro") -> 181; -entity("para") -> 182; -entity("middot") -> 183; -entity("cedil") -> 184; -entity("sup1") -> 185; -entity("ordm") -> 186; -entity("raquo") -> 187; -entity("frac14") -> 188; -entity("frac12") -> 189; -entity("frac34") -> 190; -entity("iquest") -> 191; -entity("Agrave") -> 192; -entity("Aacute") -> 193; -entity("Acirc") -> 194; -entity("Atilde") -> 195; -entity("Auml") -> 196; -entity("Aring") -> 197; -entity("AElig") -> 198; -entity("Ccedil") -> 199; -entity("Egrave") -> 200; -entity("Eacute") -> 201; -entity("Ecirc") -> 202; -entity("Euml") -> 203; -entity("Igrave") -> 204; -entity("Iacute") -> 205; -entity("Icirc") -> 206; -entity("Iuml") -> 207; -entity("ETH") -> 208; -entity("Ntilde") -> 209; -entity("Ograve") -> 210; -entity("Oacute") -> 211; -entity("Ocirc") -> 212; -entity("Otilde") -> 213; -entity("Ouml") -> 214; -entity("times") -> 215; -entity("Oslash") -> 216; -entity("Ugrave") -> 217; -entity("Uacute") -> 218; -entity("Ucirc") -> 219; -entity("Uuml") -> 220; -entity("Yacute") -> 221; -entity("THORN") -> 222; -entity("szlig") -> 223; -entity("agrave") -> 224; -entity("aacute") -> 225; -entity("acirc") -> 226; -entity("atilde") -> 227; -entity("auml") -> 228; -entity("aring") -> 229; -entity("aelig") -> 230; -entity("ccedil") -> 231; -entity("egrave") -> 232; -entity("eacute") -> 233; -entity("ecirc") -> 234; -entity("euml") -> 235; -entity("igrave") -> 236; -entity("iacute") -> 237; -entity("icirc") -> 238; -entity("iuml") -> 239; -entity("eth") -> 240; -entity("ntilde") -> 241; -entity("ograve") -> 242; -entity("oacute") -> 243; -entity("ocirc") -> 244; -entity("otilde") -> 245; -entity("ouml") -> 246; -entity("divide") -> 247; -entity("oslash") -> 248; -entity("ugrave") -> 249; -entity("uacute") -> 250; -entity("ucirc") -> 251; -entity("uuml") -> 252; -entity("yacute") -> 253; -entity("thorn") -> 254; -entity("yuml") -> 255; -entity("fnof") -> 402; -entity("Alpha") -> 913; -entity("Beta") -> 914; -entity("Gamma") -> 915; -entity("Delta") -> 916; -entity("Epsilon") -> 917; -entity("Zeta") -> 918; -entity("Eta") -> 919; -entity("Theta") -> 920; -entity("Iota") -> 921; -entity("Kappa") -> 922; -entity("Lambda") -> 923; -entity("Mu") -> 924; -entity("Nu") -> 925; -entity("Xi") -> 926; -entity("Omicron") -> 927; -entity("Pi") -> 928; -entity("Rho") -> 929; -entity("Sigma") -> 931; -entity("Tau") -> 932; -entity("Upsilon") -> 933; -entity("Phi") -> 934; -entity("Chi") -> 935; -entity("Psi") -> 936; -entity("Omega") -> 937; -entity("alpha") -> 945; -entity("beta") -> 946; -entity("gamma") -> 947; -entity("delta") -> 948; -entity("epsilon") -> 949; -entity("zeta") -> 950; -entity("eta") -> 951; -entity("theta") -> 952; -entity("iota") -> 953; -entity("kappa") -> 954; -entity("lambda") -> 955; -entity("mu") -> 956; -entity("nu") -> 957; -entity("xi") -> 958; -entity("omicron") -> 959; -entity("pi") -> 960; -entity("rho") -> 961; -entity("sigmaf") -> 962; -entity("sigma") -> 963; -entity("tau") -> 964; -entity("upsilon") -> 965; -entity("phi") -> 966; -entity("chi") -> 967; -entity("psi") -> 968; -entity("omega") -> 969; -entity("thetasym") -> 977; -entity("upsih") -> 978; -entity("piv") -> 982; -entity("bull") -> 8226; -entity("hellip") -> 8230; -entity("prime") -> 8242; -entity("Prime") -> 8243; -entity("oline") -> 8254; -entity("frasl") -> 8260; -entity("weierp") -> 8472; -entity("image") -> 8465; -entity("real") -> 8476; -entity("trade") -> 8482; -entity("alefsym") -> 8501; -entity("larr") -> 8592; -entity("uarr") -> 8593; -entity("rarr") -> 8594; -entity("darr") -> 8595; -entity("harr") -> 8596; -entity("crarr") -> 8629; -entity("lArr") -> 8656; -entity("uArr") -> 8657; -entity("rArr") -> 8658; -entity("dArr") -> 8659; -entity("hArr") -> 8660; -entity("forall") -> 8704; -entity("part") -> 8706; -entity("exist") -> 8707; -entity("empty") -> 8709; -entity("nabla") -> 8711; -entity("isin") -> 8712; -entity("notin") -> 8713; -entity("ni") -> 8715; -entity("prod") -> 8719; -entity("sum") -> 8721; -entity("minus") -> 8722; -entity("lowast") -> 8727; -entity("radic") -> 8730; -entity("prop") -> 8733; -entity("infin") -> 8734; -entity("ang") -> 8736; -entity("and") -> 8743; -entity("or") -> 8744; -entity("cap") -> 8745; -entity("cup") -> 8746; -entity("int") -> 8747; -entity("there4") -> 8756; -entity("sim") -> 8764; -entity("cong") -> 8773; -entity("asymp") -> 8776; -entity("ne") -> 8800; -entity("equiv") -> 8801; -entity("le") -> 8804; -entity("ge") -> 8805; -entity("sub") -> 8834; -entity("sup") -> 8835; -entity("nsub") -> 8836; -entity("sube") -> 8838; -entity("supe") -> 8839; -entity("oplus") -> 8853; -entity("otimes") -> 8855; -entity("perp") -> 8869; -entity("sdot") -> 8901; -entity("lceil") -> 8968; -entity("rceil") -> 8969; -entity("lfloor") -> 8970; -entity("rfloor") -> 8971; -entity("lang") -> 9001; -entity("rang") -> 9002; -entity("loz") -> 9674; -entity("spades") -> 9824; -entity("clubs") -> 9827; -entity("hearts") -> 9829; -entity("diams") -> 9830; -entity("quot") -> 34; -entity("amp") -> 38; -entity("lt") -> 60; -entity("gt") -> 62; -entity("OElig") -> 338; -entity("oelig") -> 339; -entity("Scaron") -> 352; -entity("scaron") -> 353; -entity("Yuml") -> 376; -entity("circ") -> 710; -entity("tilde") -> 732; -entity("ensp") -> 8194; -entity("emsp") -> 8195; -entity("thinsp") -> 8201; -entity("zwnj") -> 8204; -entity("zwj") -> 8205; -entity("lrm") -> 8206; -entity("rlm") -> 8207; -entity("ndash") -> 8211; -entity("mdash") -> 8212; -entity("lsquo") -> 8216; -entity("rsquo") -> 8217; -entity("sbquo") -> 8218; -entity("ldquo") -> 8220; -entity("rdquo") -> 8221; -entity("bdquo") -> 8222; -entity("dagger") -> 8224; -entity("Dagger") -> 8225; -entity("permil") -> 8240; -entity("lsaquo") -> 8249; -entity("rsaquo") -> 8250; -entity("euro") -> 8364; -entity(_) -> undefined. +%% [2011-10-14] Generated from: +%% http://www.w3.org/TR/html5/named-character-references.html +entity("AElig") -> 16#000C6; +entity("AMP") -> 16#00026; +entity("Aacute") -> 16#000C1; +entity("Abreve") -> 16#00102; +entity("Acirc") -> 16#000C2; +entity("Acy") -> 16#00410; +entity("Afr") -> 16#1D504; +entity("Agrave") -> 16#000C0; +entity("Alpha") -> 16#00391; +entity("Amacr") -> 16#00100; +entity("And") -> 16#02A53; +entity("Aogon") -> 16#00104; +entity("Aopf") -> 16#1D538; +entity("ApplyFunction") -> 16#02061; +entity("Aring") -> 16#000C5; +entity("Ascr") -> 16#1D49C; +entity("Assign") -> 16#02254; +entity("Atilde") -> 16#000C3; +entity("Auml") -> 16#000C4; +entity("Backslash") -> 16#02216; +entity("Barv") -> 16#02AE7; +entity("Barwed") -> 16#02306; +entity("Bcy") -> 16#00411; +entity("Because") -> 16#02235; +entity("Bernoullis") -> 16#0212C; +entity("Beta") -> 16#00392; +entity("Bfr") -> 16#1D505; +entity("Bopf") -> 16#1D539; +entity("Breve") -> 16#002D8; +entity("Bscr") -> 16#0212C; +entity("Bumpeq") -> 16#0224E; +entity("CHcy") -> 16#00427; +entity("COPY") -> 16#000A9; +entity("Cacute") -> 16#00106; +entity("Cap") -> 16#022D2; +entity("CapitalDifferentialD") -> 16#02145; +entity("Cayleys") -> 16#0212D; +entity("Ccaron") -> 16#0010C; +entity("Ccedil") -> 16#000C7; +entity("Ccirc") -> 16#00108; +entity("Cconint") -> 16#02230; +entity("Cdot") -> 16#0010A; +entity("Cedilla") -> 16#000B8; +entity("CenterDot") -> 16#000B7; +entity("Cfr") -> 16#0212D; +entity("Chi") -> 16#003A7; +entity("CircleDot") -> 16#02299; +entity("CircleMinus") -> 16#02296; +entity("CirclePlus") -> 16#02295; +entity("CircleTimes") -> 16#02297; +entity("ClockwiseContourIntegral") -> 16#02232; +entity("CloseCurlyDoubleQuote") -> 16#0201D; +entity("CloseCurlyQuote") -> 16#02019; +entity("Colon") -> 16#02237; +entity("Colone") -> 16#02A74; +entity("Congruent") -> 16#02261; +entity("Conint") -> 16#0222F; +entity("ContourIntegral") -> 16#0222E; +entity("Copf") -> 16#02102; +entity("Coproduct") -> 16#02210; +entity("CounterClockwiseContourIntegral") -> 16#02233; +entity("Cross") -> 16#02A2F; +entity("Cscr") -> 16#1D49E; +entity("Cup") -> 16#022D3; +entity("CupCap") -> 16#0224D; +entity("DD") -> 16#02145; +entity("DDotrahd") -> 16#02911; +entity("DJcy") -> 16#00402; +entity("DScy") -> 16#00405; +entity("DZcy") -> 16#0040F; +entity("Dagger") -> 16#02021; +entity("Darr") -> 16#021A1; +entity("Dashv") -> 16#02AE4; +entity("Dcaron") -> 16#0010E; +entity("Dcy") -> 16#00414; +entity("Del") -> 16#02207; +entity("Delta") -> 16#00394; +entity("Dfr") -> 16#1D507; +entity("DiacriticalAcute") -> 16#000B4; +entity("DiacriticalDot") -> 16#002D9; +entity("DiacriticalDoubleAcute") -> 16#002DD; +entity("DiacriticalGrave") -> 16#00060; +entity("DiacriticalTilde") -> 16#002DC; +entity("Diamond") -> 16#022C4; +entity("DifferentialD") -> 16#02146; +entity("Dopf") -> 16#1D53B; +entity("Dot") -> 16#000A8; +entity("DotDot") -> 16#020DC; +entity("DotEqual") -> 16#02250; +entity("DoubleContourIntegral") -> 16#0222F; +entity("DoubleDot") -> 16#000A8; +entity("DoubleDownArrow") -> 16#021D3; +entity("DoubleLeftArrow") -> 16#021D0; +entity("DoubleLeftRightArrow") -> 16#021D4; +entity("DoubleLeftTee") -> 16#02AE4; +entity("DoubleLongLeftArrow") -> 16#027F8; +entity("DoubleLongLeftRightArrow") -> 16#027FA; +entity("DoubleLongRightArrow") -> 16#027F9; +entity("DoubleRightArrow") -> 16#021D2; +entity("DoubleRightTee") -> 16#022A8; +entity("DoubleUpArrow") -> 16#021D1; +entity("DoubleUpDownArrow") -> 16#021D5; +entity("DoubleVerticalBar") -> 16#02225; +entity("DownArrow") -> 16#02193; +entity("DownArrowBar") -> 16#02913; +entity("DownArrowUpArrow") -> 16#021F5; +entity("DownBreve") -> 16#00311; +entity("DownLeftRightVector") -> 16#02950; +entity("DownLeftTeeVector") -> 16#0295E; +entity("DownLeftVector") -> 16#021BD; +entity("DownLeftVectorBar") -> 16#02956; +entity("DownRightTeeVector") -> 16#0295F; +entity("DownRightVector") -> 16#021C1; +entity("DownRightVectorBar") -> 16#02957; +entity("DownTee") -> 16#022A4; +entity("DownTeeArrow") -> 16#021A7; +entity("Downarrow") -> 16#021D3; +entity("Dscr") -> 16#1D49F; +entity("Dstrok") -> 16#00110; +entity("ENG") -> 16#0014A; +entity("ETH") -> 16#000D0; +entity("Eacute") -> 16#000C9; +entity("Ecaron") -> 16#0011A; +entity("Ecirc") -> 16#000CA; +entity("Ecy") -> 16#0042D; +entity("Edot") -> 16#00116; +entity("Efr") -> 16#1D508; +entity("Egrave") -> 16#000C8; +entity("Element") -> 16#02208; +entity("Emacr") -> 16#00112; +entity("EmptySmallSquare") -> 16#025FB; +entity("EmptyVerySmallSquare") -> 16#025AB; +entity("Eogon") -> 16#00118; +entity("Eopf") -> 16#1D53C; +entity("Epsilon") -> 16#00395; +entity("Equal") -> 16#02A75; +entity("EqualTilde") -> 16#02242; +entity("Equilibrium") -> 16#021CC; +entity("Escr") -> 16#02130; +entity("Esim") -> 16#02A73; +entity("Eta") -> 16#00397; +entity("Euml") -> 16#000CB; +entity("Exists") -> 16#02203; +entity("ExponentialE") -> 16#02147; +entity("Fcy") -> 16#00424; +entity("Ffr") -> 16#1D509; +entity("FilledSmallSquare") -> 16#025FC; +entity("FilledVerySmallSquare") -> 16#025AA; +entity("Fopf") -> 16#1D53D; +entity("ForAll") -> 16#02200; +entity("Fouriertrf") -> 16#02131; +entity("Fscr") -> 16#02131; +entity("GJcy") -> 16#00403; +entity("GT") -> 16#0003E; +entity("Gamma") -> 16#00393; +entity("Gammad") -> 16#003DC; +entity("Gbreve") -> 16#0011E; +entity("Gcedil") -> 16#00122; +entity("Gcirc") -> 16#0011C; +entity("Gcy") -> 16#00413; +entity("Gdot") -> 16#00120; +entity("Gfr") -> 16#1D50A; +entity("Gg") -> 16#022D9; +entity("Gopf") -> 16#1D53E; +entity("GreaterEqual") -> 16#02265; +entity("GreaterEqualLess") -> 16#022DB; +entity("GreaterFullEqual") -> 16#02267; +entity("GreaterGreater") -> 16#02AA2; +entity("GreaterLess") -> 16#02277; +entity("GreaterSlantEqual") -> 16#02A7E; +entity("GreaterTilde") -> 16#02273; +entity("Gscr") -> 16#1D4A2; +entity("Gt") -> 16#0226B; +entity("HARDcy") -> 16#0042A; +entity("Hacek") -> 16#002C7; +entity("Hat") -> 16#0005E; +entity("Hcirc") -> 16#00124; +entity("Hfr") -> 16#0210C; +entity("HilbertSpace") -> 16#0210B; +entity("Hopf") -> 16#0210D; +entity("HorizontalLine") -> 16#02500; +entity("Hscr") -> 16#0210B; +entity("Hstrok") -> 16#00126; +entity("HumpDownHump") -> 16#0224E; +entity("HumpEqual") -> 16#0224F; +entity("IEcy") -> 16#00415; +entity("IJlig") -> 16#00132; +entity("IOcy") -> 16#00401; +entity("Iacute") -> 16#000CD; +entity("Icirc") -> 16#000CE; +entity("Icy") -> 16#00418; +entity("Idot") -> 16#00130; +entity("Ifr") -> 16#02111; +entity("Igrave") -> 16#000CC; +entity("Im") -> 16#02111; +entity("Imacr") -> 16#0012A; +entity("ImaginaryI") -> 16#02148; +entity("Implies") -> 16#021D2; +entity("Int") -> 16#0222C; +entity("Integral") -> 16#0222B; +entity("Intersection") -> 16#022C2; +entity("InvisibleComma") -> 16#02063; +entity("InvisibleTimes") -> 16#02062; +entity("Iogon") -> 16#0012E; +entity("Iopf") -> 16#1D540; +entity("Iota") -> 16#00399; +entity("Iscr") -> 16#02110; +entity("Itilde") -> 16#00128; +entity("Iukcy") -> 16#00406; +entity("Iuml") -> 16#000CF; +entity("Jcirc") -> 16#00134; +entity("Jcy") -> 16#00419; +entity("Jfr") -> 16#1D50D; +entity("Jopf") -> 16#1D541; +entity("Jscr") -> 16#1D4A5; +entity("Jsercy") -> 16#00408; +entity("Jukcy") -> 16#00404; +entity("KHcy") -> 16#00425; +entity("KJcy") -> 16#0040C; +entity("Kappa") -> 16#0039A; +entity("Kcedil") -> 16#00136; +entity("Kcy") -> 16#0041A; +entity("Kfr") -> 16#1D50E; +entity("Kopf") -> 16#1D542; +entity("Kscr") -> 16#1D4A6; +entity("LJcy") -> 16#00409; +entity("LT") -> 16#0003C; +entity("Lacute") -> 16#00139; +entity("Lambda") -> 16#0039B; +entity("Lang") -> 16#027EA; +entity("Laplacetrf") -> 16#02112; +entity("Larr") -> 16#0219E; +entity("Lcaron") -> 16#0013D; +entity("Lcedil") -> 16#0013B; +entity("Lcy") -> 16#0041B; +entity("LeftAngleBracket") -> 16#027E8; +entity("LeftArrow") -> 16#02190; +entity("LeftArrowBar") -> 16#021E4; +entity("LeftArrowRightArrow") -> 16#021C6; +entity("LeftCeiling") -> 16#02308; +entity("LeftDoubleBracket") -> 16#027E6; +entity("LeftDownTeeVector") -> 16#02961; +entity("LeftDownVector") -> 16#021C3; +entity("LeftDownVectorBar") -> 16#02959; +entity("LeftFloor") -> 16#0230A; +entity("LeftRightArrow") -> 16#02194; +entity("LeftRightVector") -> 16#0294E; +entity("LeftTee") -> 16#022A3; +entity("LeftTeeArrow") -> 16#021A4; +entity("LeftTeeVector") -> 16#0295A; +entity("LeftTriangle") -> 16#022B2; +entity("LeftTriangleBar") -> 16#029CF; +entity("LeftTriangleEqual") -> 16#022B4; +entity("LeftUpDownVector") -> 16#02951; +entity("LeftUpTeeVector") -> 16#02960; +entity("LeftUpVector") -> 16#021BF; +entity("LeftUpVectorBar") -> 16#02958; +entity("LeftVector") -> 16#021BC; +entity("LeftVectorBar") -> 16#02952; +entity("Leftarrow") -> 16#021D0; +entity("Leftrightarrow") -> 16#021D4; +entity("LessEqualGreater") -> 16#022DA; +entity("LessFullEqual") -> 16#02266; +entity("LessGreater") -> 16#02276; +entity("LessLess") -> 16#02AA1; +entity("LessSlantEqual") -> 16#02A7D; +entity("LessTilde") -> 16#02272; +entity("Lfr") -> 16#1D50F; +entity("Ll") -> 16#022D8; +entity("Lleftarrow") -> 16#021DA; +entity("Lmidot") -> 16#0013F; +entity("LongLeftArrow") -> 16#027F5; +entity("LongLeftRightArrow") -> 16#027F7; +entity("LongRightArrow") -> 16#027F6; +entity("Longleftarrow") -> 16#027F8; +entity("Longleftrightarrow") -> 16#027FA; +entity("Longrightarrow") -> 16#027F9; +entity("Lopf") -> 16#1D543; +entity("LowerLeftArrow") -> 16#02199; +entity("LowerRightArrow") -> 16#02198; +entity("Lscr") -> 16#02112; +entity("Lsh") -> 16#021B0; +entity("Lstrok") -> 16#00141; +entity("Lt") -> 16#0226A; +entity("Map") -> 16#02905; +entity("Mcy") -> 16#0041C; +entity("MediumSpace") -> 16#0205F; +entity("Mellintrf") -> 16#02133; +entity("Mfr") -> 16#1D510; +entity("MinusPlus") -> 16#02213; +entity("Mopf") -> 16#1D544; +entity("Mscr") -> 16#02133; +entity("Mu") -> 16#0039C; +entity("NJcy") -> 16#0040A; +entity("Nacute") -> 16#00143; +entity("Ncaron") -> 16#00147; +entity("Ncedil") -> 16#00145; +entity("Ncy") -> 16#0041D; +entity("NegativeMediumSpace") -> 16#0200B; +entity("NegativeThickSpace") -> 16#0200B; +entity("NegativeThinSpace") -> 16#0200B; +entity("NegativeVeryThinSpace") -> 16#0200B; +entity("NestedGreaterGreater") -> 16#0226B; +entity("NestedLessLess") -> 16#0226A; +entity("NewLine") -> 16#0000A; +entity("Nfr") -> 16#1D511; +entity("NoBreak") -> 16#02060; +entity("NonBreakingSpace") -> 16#000A0; +entity("Nopf") -> 16#02115; +entity("Not") -> 16#02AEC; +entity("NotCongruent") -> 16#02262; +entity("NotCupCap") -> 16#0226D; +entity("NotDoubleVerticalBar") -> 16#02226; +entity("NotElement") -> 16#02209; +entity("NotEqual") -> 16#02260; +entity("NotEqualTilde") -> [16#02242, 16#00338]; +entity("NotExists") -> 16#02204; +entity("NotGreater") -> 16#0226F; +entity("NotGreaterEqual") -> 16#02271; +entity("NotGreaterFullEqual") -> [16#02267, 16#00338]; +entity("NotGreaterGreater") -> [16#0226B, 16#00338]; +entity("NotGreaterLess") -> 16#02279; +entity("NotGreaterSlantEqual") -> [16#02A7E, 16#00338]; +entity("NotGreaterTilde") -> 16#02275; +entity("NotHumpDownHump") -> [16#0224E, 16#00338]; +entity("NotHumpEqual") -> [16#0224F, 16#00338]; +entity("NotLeftTriangle") -> 16#022EA; +entity("NotLeftTriangleBar") -> [16#029CF, 16#00338]; +entity("NotLeftTriangleEqual") -> 16#022EC; +entity("NotLess") -> 16#0226E; +entity("NotLessEqual") -> 16#02270; +entity("NotLessGreater") -> 16#02278; +entity("NotLessLess") -> [16#0226A, 16#00338]; +entity("NotLessSlantEqual") -> [16#02A7D, 16#00338]; +entity("NotLessTilde") -> 16#02274; +entity("NotNestedGreaterGreater") -> [16#02AA2, 16#00338]; +entity("NotNestedLessLess") -> [16#02AA1, 16#00338]; +entity("NotPrecedes") -> 16#02280; +entity("NotPrecedesEqual") -> [16#02AAF, 16#00338]; +entity("NotPrecedesSlantEqual") -> 16#022E0; +entity("NotReverseElement") -> 16#0220C; +entity("NotRightTriangle") -> 16#022EB; +entity("NotRightTriangleBar") -> [16#029D0, 16#00338]; +entity("NotRightTriangleEqual") -> 16#022ED; +entity("NotSquareSubset") -> [16#0228F, 16#00338]; +entity("NotSquareSubsetEqual") -> 16#022E2; +entity("NotSquareSuperset") -> [16#02290, 16#00338]; +entity("NotSquareSupersetEqual") -> 16#022E3; +entity("NotSubset") -> [16#02282, 16#020D2]; +entity("NotSubsetEqual") -> 16#02288; +entity("NotSucceeds") -> 16#02281; +entity("NotSucceedsEqual") -> [16#02AB0, 16#00338]; +entity("NotSucceedsSlantEqual") -> 16#022E1; +entity("NotSucceedsTilde") -> [16#0227F, 16#00338]; +entity("NotSuperset") -> [16#02283, 16#020D2]; +entity("NotSupersetEqual") -> 16#02289; +entity("NotTilde") -> 16#02241; +entity("NotTildeEqual") -> 16#02244; +entity("NotTildeFullEqual") -> 16#02247; +entity("NotTildeTilde") -> 16#02249; +entity("NotVerticalBar") -> 16#02224; +entity("Nscr") -> 16#1D4A9; +entity("Ntilde") -> 16#000D1; +entity("Nu") -> 16#0039D; +entity("OElig") -> 16#00152; +entity("Oacute") -> 16#000D3; +entity("Ocirc") -> 16#000D4; +entity("Ocy") -> 16#0041E; +entity("Odblac") -> 16#00150; +entity("Ofr") -> 16#1D512; +entity("Ograve") -> 16#000D2; +entity("Omacr") -> 16#0014C; +entity("Omega") -> 16#003A9; +entity("Omicron") -> 16#0039F; +entity("Oopf") -> 16#1D546; +entity("OpenCurlyDoubleQuote") -> 16#0201C; +entity("OpenCurlyQuote") -> 16#02018; +entity("Or") -> 16#02A54; +entity("Oscr") -> 16#1D4AA; +entity("Oslash") -> 16#000D8; +entity("Otilde") -> 16#000D5; +entity("Otimes") -> 16#02A37; +entity("Ouml") -> 16#000D6; +entity("OverBar") -> 16#0203E; +entity("OverBrace") -> 16#023DE; +entity("OverBracket") -> 16#023B4; +entity("OverParenthesis") -> 16#023DC; +entity("PartialD") -> 16#02202; +entity("Pcy") -> 16#0041F; +entity("Pfr") -> 16#1D513; +entity("Phi") -> 16#003A6; +entity("Pi") -> 16#003A0; +entity("PlusMinus") -> 16#000B1; +entity("Poincareplane") -> 16#0210C; +entity("Popf") -> 16#02119; +entity("Pr") -> 16#02ABB; +entity("Precedes") -> 16#0227A; +entity("PrecedesEqual") -> 16#02AAF; +entity("PrecedesSlantEqual") -> 16#0227C; +entity("PrecedesTilde") -> 16#0227E; +entity("Prime") -> 16#02033; +entity("Product") -> 16#0220F; +entity("Proportion") -> 16#02237; +entity("Proportional") -> 16#0221D; +entity("Pscr") -> 16#1D4AB; +entity("Psi") -> 16#003A8; +entity("QUOT") -> 16#00022; +entity("Qfr") -> 16#1D514; +entity("Qopf") -> 16#0211A; +entity("Qscr") -> 16#1D4AC; +entity("RBarr") -> 16#02910; +entity("REG") -> 16#000AE; +entity("Racute") -> 16#00154; +entity("Rang") -> 16#027EB; +entity("Rarr") -> 16#021A0; +entity("Rarrtl") -> 16#02916; +entity("Rcaron") -> 16#00158; +entity("Rcedil") -> 16#00156; +entity("Rcy") -> 16#00420; +entity("Re") -> 16#0211C; +entity("ReverseElement") -> 16#0220B; +entity("ReverseEquilibrium") -> 16#021CB; +entity("ReverseUpEquilibrium") -> 16#0296F; +entity("Rfr") -> 16#0211C; +entity("Rho") -> 16#003A1; +entity("RightAngleBracket") -> 16#027E9; +entity("RightArrow") -> 16#02192; +entity("RightArrowBar") -> 16#021E5; +entity("RightArrowLeftArrow") -> 16#021C4; +entity("RightCeiling") -> 16#02309; +entity("RightDoubleBracket") -> 16#027E7; +entity("RightDownTeeVector") -> 16#0295D; +entity("RightDownVector") -> 16#021C2; +entity("RightDownVectorBar") -> 16#02955; +entity("RightFloor") -> 16#0230B; +entity("RightTee") -> 16#022A2; +entity("RightTeeArrow") -> 16#021A6; +entity("RightTeeVector") -> 16#0295B; +entity("RightTriangle") -> 16#022B3; +entity("RightTriangleBar") -> 16#029D0; +entity("RightTriangleEqual") -> 16#022B5; +entity("RightUpDownVector") -> 16#0294F; +entity("RightUpTeeVector") -> 16#0295C; +entity("RightUpVector") -> 16#021BE; +entity("RightUpVectorBar") -> 16#02954; +entity("RightVector") -> 16#021C0; +entity("RightVectorBar") -> 16#02953; +entity("Rightarrow") -> 16#021D2; +entity("Ropf") -> 16#0211D; +entity("RoundImplies") -> 16#02970; +entity("Rrightarrow") -> 16#021DB; +entity("Rscr") -> 16#0211B; +entity("Rsh") -> 16#021B1; +entity("RuleDelayed") -> 16#029F4; +entity("SHCHcy") -> 16#00429; +entity("SHcy") -> 16#00428; +entity("SOFTcy") -> 16#0042C; +entity("Sacute") -> 16#0015A; +entity("Sc") -> 16#02ABC; +entity("Scaron") -> 16#00160; +entity("Scedil") -> 16#0015E; +entity("Scirc") -> 16#0015C; +entity("Scy") -> 16#00421; +entity("Sfr") -> 16#1D516; +entity("ShortDownArrow") -> 16#02193; +entity("ShortLeftArrow") -> 16#02190; +entity("ShortRightArrow") -> 16#02192; +entity("ShortUpArrow") -> 16#02191; +entity("Sigma") -> 16#003A3; +entity("SmallCircle") -> 16#02218; +entity("Sopf") -> 16#1D54A; +entity("Sqrt") -> 16#0221A; +entity("Square") -> 16#025A1; +entity("SquareIntersection") -> 16#02293; +entity("SquareSubset") -> 16#0228F; +entity("SquareSubsetEqual") -> 16#02291; +entity("SquareSuperset") -> 16#02290; +entity("SquareSupersetEqual") -> 16#02292; +entity("SquareUnion") -> 16#02294; +entity("Sscr") -> 16#1D4AE; +entity("Star") -> 16#022C6; +entity("Sub") -> 16#022D0; +entity("Subset") -> 16#022D0; +entity("SubsetEqual") -> 16#02286; +entity("Succeeds") -> 16#0227B; +entity("SucceedsEqual") -> 16#02AB0; +entity("SucceedsSlantEqual") -> 16#0227D; +entity("SucceedsTilde") -> 16#0227F; +entity("SuchThat") -> 16#0220B; +entity("Sum") -> 16#02211; +entity("Sup") -> 16#022D1; +entity("Superset") -> 16#02283; +entity("SupersetEqual") -> 16#02287; +entity("Supset") -> 16#022D1; +entity("THORN") -> 16#000DE; +entity("TRADE") -> 16#02122; +entity("TSHcy") -> 16#0040B; +entity("TScy") -> 16#00426; +entity("Tab") -> 16#00009; +entity("Tau") -> 16#003A4; +entity("Tcaron") -> 16#00164; +entity("Tcedil") -> 16#00162; +entity("Tcy") -> 16#00422; +entity("Tfr") -> 16#1D517; +entity("Therefore") -> 16#02234; +entity("Theta") -> 16#00398; +entity("ThickSpace") -> [16#0205F, 16#0200A]; +entity("ThinSpace") -> 16#02009; +entity("Tilde") -> 16#0223C; +entity("TildeEqual") -> 16#02243; +entity("TildeFullEqual") -> 16#02245; +entity("TildeTilde") -> 16#02248; +entity("Topf") -> 16#1D54B; +entity("TripleDot") -> 16#020DB; +entity("Tscr") -> 16#1D4AF; +entity("Tstrok") -> 16#00166; +entity("Uacute") -> 16#000DA; +entity("Uarr") -> 16#0219F; +entity("Uarrocir") -> 16#02949; +entity("Ubrcy") -> 16#0040E; +entity("Ubreve") -> 16#0016C; +entity("Ucirc") -> 16#000DB; +entity("Ucy") -> 16#00423; +entity("Udblac") -> 16#00170; +entity("Ufr") -> 16#1D518; +entity("Ugrave") -> 16#000D9; +entity("Umacr") -> 16#0016A; +entity("UnderBar") -> 16#0005F; +entity("UnderBrace") -> 16#023DF; +entity("UnderBracket") -> 16#023B5; +entity("UnderParenthesis") -> 16#023DD; +entity("Union") -> 16#022C3; +entity("UnionPlus") -> 16#0228E; +entity("Uogon") -> 16#00172; +entity("Uopf") -> 16#1D54C; +entity("UpArrow") -> 16#02191; +entity("UpArrowBar") -> 16#02912; +entity("UpArrowDownArrow") -> 16#021C5; +entity("UpDownArrow") -> 16#02195; +entity("UpEquilibrium") -> 16#0296E; +entity("UpTee") -> 16#022A5; +entity("UpTeeArrow") -> 16#021A5; +entity("Uparrow") -> 16#021D1; +entity("Updownarrow") -> 16#021D5; +entity("UpperLeftArrow") -> 16#02196; +entity("UpperRightArrow") -> 16#02197; +entity("Upsi") -> 16#003D2; +entity("Upsilon") -> 16#003A5; +entity("Uring") -> 16#0016E; +entity("Uscr") -> 16#1D4B0; +entity("Utilde") -> 16#00168; +entity("Uuml") -> 16#000DC; +entity("VDash") -> 16#022AB; +entity("Vbar") -> 16#02AEB; +entity("Vcy") -> 16#00412; +entity("Vdash") -> 16#022A9; +entity("Vdashl") -> 16#02AE6; +entity("Vee") -> 16#022C1; +entity("Verbar") -> 16#02016; +entity("Vert") -> 16#02016; +entity("VerticalBar") -> 16#02223; +entity("VerticalLine") -> 16#0007C; +entity("VerticalSeparator") -> 16#02758; +entity("VerticalTilde") -> 16#02240; +entity("VeryThinSpace") -> 16#0200A; +entity("Vfr") -> 16#1D519; +entity("Vopf") -> 16#1D54D; +entity("Vscr") -> 16#1D4B1; +entity("Vvdash") -> 16#022AA; +entity("Wcirc") -> 16#00174; +entity("Wedge") -> 16#022C0; +entity("Wfr") -> 16#1D51A; +entity("Wopf") -> 16#1D54E; +entity("Wscr") -> 16#1D4B2; +entity("Xfr") -> 16#1D51B; +entity("Xi") -> 16#0039E; +entity("Xopf") -> 16#1D54F; +entity("Xscr") -> 16#1D4B3; +entity("YAcy") -> 16#0042F; +entity("YIcy") -> 16#00407; +entity("YUcy") -> 16#0042E; +entity("Yacute") -> 16#000DD; +entity("Ycirc") -> 16#00176; +entity("Ycy") -> 16#0042B; +entity("Yfr") -> 16#1D51C; +entity("Yopf") -> 16#1D550; +entity("Yscr") -> 16#1D4B4; +entity("Yuml") -> 16#00178; +entity("ZHcy") -> 16#00416; +entity("Zacute") -> 16#00179; +entity("Zcaron") -> 16#0017D; +entity("Zcy") -> 16#00417; +entity("Zdot") -> 16#0017B; +entity("ZeroWidthSpace") -> 16#0200B; +entity("Zeta") -> 16#00396; +entity("Zfr") -> 16#02128; +entity("Zopf") -> 16#02124; +entity("Zscr") -> 16#1D4B5; +entity("aacute") -> 16#000E1; +entity("abreve") -> 16#00103; +entity("ac") -> 16#0223E; +entity("acE") -> [16#0223E, 16#00333]; +entity("acd") -> 16#0223F; +entity("acirc") -> 16#000E2; +entity("acute") -> 16#000B4; +entity("acy") -> 16#00430; +entity("aelig") -> 16#000E6; +entity("af") -> 16#02061; +entity("afr") -> 16#1D51E; +entity("agrave") -> 16#000E0; +entity("alefsym") -> 16#02135; +entity("aleph") -> 16#02135; +entity("alpha") -> 16#003B1; +entity("amacr") -> 16#00101; +entity("amalg") -> 16#02A3F; +entity("amp") -> 16#00026; +entity("and") -> 16#02227; +entity("andand") -> 16#02A55; +entity("andd") -> 16#02A5C; +entity("andslope") -> 16#02A58; +entity("andv") -> 16#02A5A; +entity("ang") -> 16#02220; +entity("ange") -> 16#029A4; +entity("angle") -> 16#02220; +entity("angmsd") -> 16#02221; +entity("angmsdaa") -> 16#029A8; +entity("angmsdab") -> 16#029A9; +entity("angmsdac") -> 16#029AA; +entity("angmsdad") -> 16#029AB; +entity("angmsdae") -> 16#029AC; +entity("angmsdaf") -> 16#029AD; +entity("angmsdag") -> 16#029AE; +entity("angmsdah") -> 16#029AF; +entity("angrt") -> 16#0221F; +entity("angrtvb") -> 16#022BE; +entity("angrtvbd") -> 16#0299D; +entity("angsph") -> 16#02222; +entity("angst") -> 16#000C5; +entity("angzarr") -> 16#0237C; +entity("aogon") -> 16#00105; +entity("aopf") -> 16#1D552; +entity("ap") -> 16#02248; +entity("apE") -> 16#02A70; +entity("apacir") -> 16#02A6F; +entity("ape") -> 16#0224A; +entity("apid") -> 16#0224B; +entity("apos") -> 16#00027; +entity("approx") -> 16#02248; +entity("approxeq") -> 16#0224A; +entity("aring") -> 16#000E5; +entity("ascr") -> 16#1D4B6; +entity("ast") -> 16#0002A; +entity("asymp") -> 16#02248; +entity("asympeq") -> 16#0224D; +entity("atilde") -> 16#000E3; +entity("auml") -> 16#000E4; +entity("awconint") -> 16#02233; +entity("awint") -> 16#02A11; +entity("bNot") -> 16#02AED; +entity("backcong") -> 16#0224C; +entity("backepsilon") -> 16#003F6; +entity("backprime") -> 16#02035; +entity("backsim") -> 16#0223D; +entity("backsimeq") -> 16#022CD; +entity("barvee") -> 16#022BD; +entity("barwed") -> 16#02305; +entity("barwedge") -> 16#02305; +entity("bbrk") -> 16#023B5; +entity("bbrktbrk") -> 16#023B6; +entity("bcong") -> 16#0224C; +entity("bcy") -> 16#00431; +entity("bdquo") -> 16#0201E; +entity("becaus") -> 16#02235; +entity("because") -> 16#02235; +entity("bemptyv") -> 16#029B0; +entity("bepsi") -> 16#003F6; +entity("bernou") -> 16#0212C; +entity("beta") -> 16#003B2; +entity("beth") -> 16#02136; +entity("between") -> 16#0226C; +entity("bfr") -> 16#1D51F; +entity("bigcap") -> 16#022C2; +entity("bigcirc") -> 16#025EF; +entity("bigcup") -> 16#022C3; +entity("bigodot") -> 16#02A00; +entity("bigoplus") -> 16#02A01; +entity("bigotimes") -> 16#02A02; +entity("bigsqcup") -> 16#02A06; +entity("bigstar") -> 16#02605; +entity("bigtriangledown") -> 16#025BD; +entity("bigtriangleup") -> 16#025B3; +entity("biguplus") -> 16#02A04; +entity("bigvee") -> 16#022C1; +entity("bigwedge") -> 16#022C0; +entity("bkarow") -> 16#0290D; +entity("blacklozenge") -> 16#029EB; +entity("blacksquare") -> 16#025AA; +entity("blacktriangle") -> 16#025B4; +entity("blacktriangledown") -> 16#025BE; +entity("blacktriangleleft") -> 16#025C2; +entity("blacktriangleright") -> 16#025B8; +entity("blank") -> 16#02423; +entity("blk12") -> 16#02592; +entity("blk14") -> 16#02591; +entity("blk34") -> 16#02593; +entity("block") -> 16#02588; +entity("bne") -> [16#0003D, 16#020E5]; +entity("bnequiv") -> [16#02261, 16#020E5]; +entity("bnot") -> 16#02310; +entity("bopf") -> 16#1D553; +entity("bot") -> 16#022A5; +entity("bottom") -> 16#022A5; +entity("bowtie") -> 16#022C8; +entity("boxDL") -> 16#02557; +entity("boxDR") -> 16#02554; +entity("boxDl") -> 16#02556; +entity("boxDr") -> 16#02553; +entity("boxH") -> 16#02550; +entity("boxHD") -> 16#02566; +entity("boxHU") -> 16#02569; +entity("boxHd") -> 16#02564; +entity("boxHu") -> 16#02567; +entity("boxUL") -> 16#0255D; +entity("boxUR") -> 16#0255A; +entity("boxUl") -> 16#0255C; +entity("boxUr") -> 16#02559; +entity("boxV") -> 16#02551; +entity("boxVH") -> 16#0256C; +entity("boxVL") -> 16#02563; +entity("boxVR") -> 16#02560; +entity("boxVh") -> 16#0256B; +entity("boxVl") -> 16#02562; +entity("boxVr") -> 16#0255F; +entity("boxbox") -> 16#029C9; +entity("boxdL") -> 16#02555; +entity("boxdR") -> 16#02552; +entity("boxdl") -> 16#02510; +entity("boxdr") -> 16#0250C; +entity("boxh") -> 16#02500; +entity("boxhD") -> 16#02565; +entity("boxhU") -> 16#02568; +entity("boxhd") -> 16#0252C; +entity("boxhu") -> 16#02534; +entity("boxminus") -> 16#0229F; +entity("boxplus") -> 16#0229E; +entity("boxtimes") -> 16#022A0; +entity("boxuL") -> 16#0255B; +entity("boxuR") -> 16#02558; +entity("boxul") -> 16#02518; +entity("boxur") -> 16#02514; +entity("boxv") -> 16#02502; +entity("boxvH") -> 16#0256A; +entity("boxvL") -> 16#02561; +entity("boxvR") -> 16#0255E; +entity("boxvh") -> 16#0253C; +entity("boxvl") -> 16#02524; +entity("boxvr") -> 16#0251C; +entity("bprime") -> 16#02035; +entity("breve") -> 16#002D8; +entity("brvbar") -> 16#000A6; +entity("bscr") -> 16#1D4B7; +entity("bsemi") -> 16#0204F; +entity("bsim") -> 16#0223D; +entity("bsime") -> 16#022CD; +entity("bsol") -> 16#0005C; +entity("bsolb") -> 16#029C5; +entity("bsolhsub") -> 16#027C8; +entity("bull") -> 16#02022; +entity("bullet") -> 16#02022; +entity("bump") -> 16#0224E; +entity("bumpE") -> 16#02AAE; +entity("bumpe") -> 16#0224F; +entity("bumpeq") -> 16#0224F; +entity("cacute") -> 16#00107; +entity("cap") -> 16#02229; +entity("capand") -> 16#02A44; +entity("capbrcup") -> 16#02A49; +entity("capcap") -> 16#02A4B; +entity("capcup") -> 16#02A47; +entity("capdot") -> 16#02A40; +entity("caps") -> [16#02229, 16#0FE00]; +entity("caret") -> 16#02041; +entity("caron") -> 16#002C7; +entity("ccaps") -> 16#02A4D; +entity("ccaron") -> 16#0010D; +entity("ccedil") -> 16#000E7; +entity("ccirc") -> 16#00109; +entity("ccups") -> 16#02A4C; +entity("ccupssm") -> 16#02A50; +entity("cdot") -> 16#0010B; +entity("cedil") -> 16#000B8; +entity("cemptyv") -> 16#029B2; +entity("cent") -> 16#000A2; +entity("centerdot") -> 16#000B7; +entity("cfr") -> 16#1D520; +entity("chcy") -> 16#00447; +entity("check") -> 16#02713; +entity("checkmark") -> 16#02713; +entity("chi") -> 16#003C7; +entity("cir") -> 16#025CB; +entity("cirE") -> 16#029C3; +entity("circ") -> 16#002C6; +entity("circeq") -> 16#02257; +entity("circlearrowleft") -> 16#021BA; +entity("circlearrowright") -> 16#021BB; +entity("circledR") -> 16#000AE; +entity("circledS") -> 16#024C8; +entity("circledast") -> 16#0229B; +entity("circledcirc") -> 16#0229A; +entity("circleddash") -> 16#0229D; +entity("cire") -> 16#02257; +entity("cirfnint") -> 16#02A10; +entity("cirmid") -> 16#02AEF; +entity("cirscir") -> 16#029C2; +entity("clubs") -> 16#02663; +entity("clubsuit") -> 16#02663; +entity("colon") -> 16#0003A; +entity("colone") -> 16#02254; +entity("coloneq") -> 16#02254; +entity("comma") -> 16#0002C; +entity("commat") -> 16#00040; +entity("comp") -> 16#02201; +entity("compfn") -> 16#02218; +entity("complement") -> 16#02201; +entity("complexes") -> 16#02102; +entity("cong") -> 16#02245; +entity("congdot") -> 16#02A6D; +entity("conint") -> 16#0222E; +entity("copf") -> 16#1D554; +entity("coprod") -> 16#02210; +entity("copy") -> 16#000A9; +entity("copysr") -> 16#02117; +entity("crarr") -> 16#021B5; +entity("cross") -> 16#02717; +entity("cscr") -> 16#1D4B8; +entity("csub") -> 16#02ACF; +entity("csube") -> 16#02AD1; +entity("csup") -> 16#02AD0; +entity("csupe") -> 16#02AD2; +entity("ctdot") -> 16#022EF; +entity("cudarrl") -> 16#02938; +entity("cudarrr") -> 16#02935; +entity("cuepr") -> 16#022DE; +entity("cuesc") -> 16#022DF; +entity("cularr") -> 16#021B6; +entity("cularrp") -> 16#0293D; +entity("cup") -> 16#0222A; +entity("cupbrcap") -> 16#02A48; +entity("cupcap") -> 16#02A46; +entity("cupcup") -> 16#02A4A; +entity("cupdot") -> 16#0228D; +entity("cupor") -> 16#02A45; +entity("cups") -> [16#0222A, 16#0FE00]; +entity("curarr") -> 16#021B7; +entity("curarrm") -> 16#0293C; +entity("curlyeqprec") -> 16#022DE; +entity("curlyeqsucc") -> 16#022DF; +entity("curlyvee") -> 16#022CE; +entity("curlywedge") -> 16#022CF; +entity("curren") -> 16#000A4; +entity("curvearrowleft") -> 16#021B6; +entity("curvearrowright") -> 16#021B7; +entity("cuvee") -> 16#022CE; +entity("cuwed") -> 16#022CF; +entity("cwconint") -> 16#02232; +entity("cwint") -> 16#02231; +entity("cylcty") -> 16#0232D; +entity("dArr") -> 16#021D3; +entity("dHar") -> 16#02965; +entity("dagger") -> 16#02020; +entity("daleth") -> 16#02138; +entity("darr") -> 16#02193; +entity("dash") -> 16#02010; +entity("dashv") -> 16#022A3; +entity("dbkarow") -> 16#0290F; +entity("dblac") -> 16#002DD; +entity("dcaron") -> 16#0010F; +entity("dcy") -> 16#00434; +entity("dd") -> 16#02146; +entity("ddagger") -> 16#02021; +entity("ddarr") -> 16#021CA; +entity("ddotseq") -> 16#02A77; +entity("deg") -> 16#000B0; +entity("delta") -> 16#003B4; +entity("demptyv") -> 16#029B1; +entity("dfisht") -> 16#0297F; +entity("dfr") -> 16#1D521; +entity("dharl") -> 16#021C3; +entity("dharr") -> 16#021C2; +entity("diam") -> 16#022C4; +entity("diamond") -> 16#022C4; +entity("diamondsuit") -> 16#02666; +entity("diams") -> 16#02666; +entity("die") -> 16#000A8; +entity("digamma") -> 16#003DD; +entity("disin") -> 16#022F2; +entity("div") -> 16#000F7; +entity("divide") -> 16#000F7; +entity("divideontimes") -> 16#022C7; +entity("divonx") -> 16#022C7; +entity("djcy") -> 16#00452; +entity("dlcorn") -> 16#0231E; +entity("dlcrop") -> 16#0230D; +entity("dollar") -> 16#00024; +entity("dopf") -> 16#1D555; +entity("dot") -> 16#002D9; +entity("doteq") -> 16#02250; +entity("doteqdot") -> 16#02251; +entity("dotminus") -> 16#02238; +entity("dotplus") -> 16#02214; +entity("dotsquare") -> 16#022A1; +entity("doublebarwedge") -> 16#02306; +entity("downarrow") -> 16#02193; +entity("downdownarrows") -> 16#021CA; +entity("downharpoonleft") -> 16#021C3; +entity("downharpoonright") -> 16#021C2; +entity("drbkarow") -> 16#02910; +entity("drcorn") -> 16#0231F; +entity("drcrop") -> 16#0230C; +entity("dscr") -> 16#1D4B9; +entity("dscy") -> 16#00455; +entity("dsol") -> 16#029F6; +entity("dstrok") -> 16#00111; +entity("dtdot") -> 16#022F1; +entity("dtri") -> 16#025BF; +entity("dtrif") -> 16#025BE; +entity("duarr") -> 16#021F5; +entity("duhar") -> 16#0296F; +entity("dwangle") -> 16#029A6; +entity("dzcy") -> 16#0045F; +entity("dzigrarr") -> 16#027FF; +entity("eDDot") -> 16#02A77; +entity("eDot") -> 16#02251; +entity("eacute") -> 16#000E9; +entity("easter") -> 16#02A6E; +entity("ecaron") -> 16#0011B; +entity("ecir") -> 16#02256; +entity("ecirc") -> 16#000EA; +entity("ecolon") -> 16#02255; +entity("ecy") -> 16#0044D; +entity("edot") -> 16#00117; +entity("ee") -> 16#02147; +entity("efDot") -> 16#02252; +entity("efr") -> 16#1D522; +entity("eg") -> 16#02A9A; +entity("egrave") -> 16#000E8; +entity("egs") -> 16#02A96; +entity("egsdot") -> 16#02A98; +entity("el") -> 16#02A99; +entity("elinters") -> 16#023E7; +entity("ell") -> 16#02113; +entity("els") -> 16#02A95; +entity("elsdot") -> 16#02A97; +entity("emacr") -> 16#00113; +entity("empty") -> 16#02205; +entity("emptyset") -> 16#02205; +entity("emptyv") -> 16#02205; +entity("emsp") -> 16#02003; +entity("emsp13") -> 16#02004; +entity("emsp14") -> 16#02005; +entity("eng") -> 16#0014B; +entity("ensp") -> 16#02002; +entity("eogon") -> 16#00119; +entity("eopf") -> 16#1D556; +entity("epar") -> 16#022D5; +entity("eparsl") -> 16#029E3; +entity("eplus") -> 16#02A71; +entity("epsi") -> 16#003B5; +entity("epsilon") -> 16#003B5; +entity("epsiv") -> 16#003F5; +entity("eqcirc") -> 16#02256; +entity("eqcolon") -> 16#02255; +entity("eqsim") -> 16#02242; +entity("eqslantgtr") -> 16#02A96; +entity("eqslantless") -> 16#02A95; +entity("equals") -> 16#0003D; +entity("equest") -> 16#0225F; +entity("equiv") -> 16#02261; +entity("equivDD") -> 16#02A78; +entity("eqvparsl") -> 16#029E5; +entity("erDot") -> 16#02253; +entity("erarr") -> 16#02971; +entity("escr") -> 16#0212F; +entity("esdot") -> 16#02250; +entity("esim") -> 16#02242; +entity("eta") -> 16#003B7; +entity("eth") -> 16#000F0; +entity("euml") -> 16#000EB; +entity("euro") -> 16#020AC; +entity("excl") -> 16#00021; +entity("exist") -> 16#02203; +entity("expectation") -> 16#02130; +entity("exponentiale") -> 16#02147; +entity("fallingdotseq") -> 16#02252; +entity("fcy") -> 16#00444; +entity("female") -> 16#02640; +entity("ffilig") -> 16#0FB03; +entity("fflig") -> 16#0FB00; +entity("ffllig") -> 16#0FB04; +entity("ffr") -> 16#1D523; +entity("filig") -> 16#0FB01; +entity("fjlig") -> [16#00066, 16#0006A]; +entity("flat") -> 16#0266D; +entity("fllig") -> 16#0FB02; +entity("fltns") -> 16#025B1; +entity("fnof") -> 16#00192; +entity("fopf") -> 16#1D557; +entity("forall") -> 16#02200; +entity("fork") -> 16#022D4; +entity("forkv") -> 16#02AD9; +entity("fpartint") -> 16#02A0D; +entity("frac12") -> 16#000BD; +entity("frac13") -> 16#02153; +entity("frac14") -> 16#000BC; +entity("frac15") -> 16#02155; +entity("frac16") -> 16#02159; +entity("frac18") -> 16#0215B; +entity("frac23") -> 16#02154; +entity("frac25") -> 16#02156; +entity("frac34") -> 16#000BE; +entity("frac35") -> 16#02157; +entity("frac38") -> 16#0215C; +entity("frac45") -> 16#02158; +entity("frac56") -> 16#0215A; +entity("frac58") -> 16#0215D; +entity("frac78") -> 16#0215E; +entity("frasl") -> 16#02044; +entity("frown") -> 16#02322; +entity("fscr") -> 16#1D4BB; +entity("gE") -> 16#02267; +entity("gEl") -> 16#02A8C; +entity("gacute") -> 16#001F5; +entity("gamma") -> 16#003B3; +entity("gammad") -> 16#003DD; +entity("gap") -> 16#02A86; +entity("gbreve") -> 16#0011F; +entity("gcirc") -> 16#0011D; +entity("gcy") -> 16#00433; +entity("gdot") -> 16#00121; +entity("ge") -> 16#02265; +entity("gel") -> 16#022DB; +entity("geq") -> 16#02265; +entity("geqq") -> 16#02267; +entity("geqslant") -> 16#02A7E; +entity("ges") -> 16#02A7E; +entity("gescc") -> 16#02AA9; +entity("gesdot") -> 16#02A80; +entity("gesdoto") -> 16#02A82; +entity("gesdotol") -> 16#02A84; +entity("gesl") -> [16#022DB, 16#0FE00]; +entity("gesles") -> 16#02A94; +entity("gfr") -> 16#1D524; +entity("gg") -> 16#0226B; +entity("ggg") -> 16#022D9; +entity("gimel") -> 16#02137; +entity("gjcy") -> 16#00453; +entity("gl") -> 16#02277; +entity("glE") -> 16#02A92; +entity("gla") -> 16#02AA5; +entity("glj") -> 16#02AA4; +entity("gnE") -> 16#02269; +entity("gnap") -> 16#02A8A; +entity("gnapprox") -> 16#02A8A; +entity("gne") -> 16#02A88; +entity("gneq") -> 16#02A88; +entity("gneqq") -> 16#02269; +entity("gnsim") -> 16#022E7; +entity("gopf") -> 16#1D558; +entity("grave") -> 16#00060; +entity("gscr") -> 16#0210A; +entity("gsim") -> 16#02273; +entity("gsime") -> 16#02A8E; +entity("gsiml") -> 16#02A90; +entity("gt") -> 16#0003E; +entity("gtcc") -> 16#02AA7; +entity("gtcir") -> 16#02A7A; +entity("gtdot") -> 16#022D7; +entity("gtlPar") -> 16#02995; +entity("gtquest") -> 16#02A7C; +entity("gtrapprox") -> 16#02A86; +entity("gtrarr") -> 16#02978; +entity("gtrdot") -> 16#022D7; +entity("gtreqless") -> 16#022DB; +entity("gtreqqless") -> 16#02A8C; +entity("gtrless") -> 16#02277; +entity("gtrsim") -> 16#02273; +entity("gvertneqq") -> [16#02269, 16#0FE00]; +entity("gvnE") -> [16#02269, 16#0FE00]; +entity("hArr") -> 16#021D4; +entity("hairsp") -> 16#0200A; +entity("half") -> 16#000BD; +entity("hamilt") -> 16#0210B; +entity("hardcy") -> 16#0044A; +entity("harr") -> 16#02194; +entity("harrcir") -> 16#02948; +entity("harrw") -> 16#021AD; +entity("hbar") -> 16#0210F; +entity("hcirc") -> 16#00125; +entity("hearts") -> 16#02665; +entity("heartsuit") -> 16#02665; +entity("hellip") -> 16#02026; +entity("hercon") -> 16#022B9; +entity("hfr") -> 16#1D525; +entity("hksearow") -> 16#02925; +entity("hkswarow") -> 16#02926; +entity("hoarr") -> 16#021FF; +entity("homtht") -> 16#0223B; +entity("hookleftarrow") -> 16#021A9; +entity("hookrightarrow") -> 16#021AA; +entity("hopf") -> 16#1D559; +entity("horbar") -> 16#02015; +entity("hscr") -> 16#1D4BD; +entity("hslash") -> 16#0210F; +entity("hstrok") -> 16#00127; +entity("hybull") -> 16#02043; +entity("hyphen") -> 16#02010; +entity("iacute") -> 16#000ED; +entity("ic") -> 16#02063; +entity("icirc") -> 16#000EE; +entity("icy") -> 16#00438; +entity("iecy") -> 16#00435; +entity("iexcl") -> 16#000A1; +entity("iff") -> 16#021D4; +entity("ifr") -> 16#1D526; +entity("igrave") -> 16#000EC; +entity("ii") -> 16#02148; +entity("iiiint") -> 16#02A0C; +entity("iiint") -> 16#0222D; +entity("iinfin") -> 16#029DC; +entity("iiota") -> 16#02129; +entity("ijlig") -> 16#00133; +entity("imacr") -> 16#0012B; +entity("image") -> 16#02111; +entity("imagline") -> 16#02110; +entity("imagpart") -> 16#02111; +entity("imath") -> 16#00131; +entity("imof") -> 16#022B7; +entity("imped") -> 16#001B5; +entity("in") -> 16#02208; +entity("incare") -> 16#02105; +entity("infin") -> 16#0221E; +entity("infintie") -> 16#029DD; +entity("inodot") -> 16#00131; +entity("int") -> 16#0222B; +entity("intcal") -> 16#022BA; +entity("integers") -> 16#02124; +entity("intercal") -> 16#022BA; +entity("intlarhk") -> 16#02A17; +entity("intprod") -> 16#02A3C; +entity("iocy") -> 16#00451; +entity("iogon") -> 16#0012F; +entity("iopf") -> 16#1D55A; +entity("iota") -> 16#003B9; +entity("iprod") -> 16#02A3C; +entity("iquest") -> 16#000BF; +entity("iscr") -> 16#1D4BE; +entity("isin") -> 16#02208; +entity("isinE") -> 16#022F9; +entity("isindot") -> 16#022F5; +entity("isins") -> 16#022F4; +entity("isinsv") -> 16#022F3; +entity("isinv") -> 16#02208; +entity("it") -> 16#02062; +entity("itilde") -> 16#00129; +entity("iukcy") -> 16#00456; +entity("iuml") -> 16#000EF; +entity("jcirc") -> 16#00135; +entity("jcy") -> 16#00439; +entity("jfr") -> 16#1D527; +entity("jmath") -> 16#00237; +entity("jopf") -> 16#1D55B; +entity("jscr") -> 16#1D4BF; +entity("jsercy") -> 16#00458; +entity("jukcy") -> 16#00454; +entity("kappa") -> 16#003BA; +entity("kappav") -> 16#003F0; +entity("kcedil") -> 16#00137; +entity("kcy") -> 16#0043A; +entity("kfr") -> 16#1D528; +entity("kgreen") -> 16#00138; +entity("khcy") -> 16#00445; +entity("kjcy") -> 16#0045C; +entity("kopf") -> 16#1D55C; +entity("kscr") -> 16#1D4C0; +entity("lAarr") -> 16#021DA; +entity("lArr") -> 16#021D0; +entity("lAtail") -> 16#0291B; +entity("lBarr") -> 16#0290E; +entity("lE") -> 16#02266; +entity("lEg") -> 16#02A8B; +entity("lHar") -> 16#02962; +entity("lacute") -> 16#0013A; +entity("laemptyv") -> 16#029B4; +entity("lagran") -> 16#02112; +entity("lambda") -> 16#003BB; +entity("lang") -> 16#027E8; +entity("langd") -> 16#02991; +entity("langle") -> 16#027E8; +entity("lap") -> 16#02A85; +entity("laquo") -> 16#000AB; +entity("larr") -> 16#02190; +entity("larrb") -> 16#021E4; +entity("larrbfs") -> 16#0291F; +entity("larrfs") -> 16#0291D; +entity("larrhk") -> 16#021A9; +entity("larrlp") -> 16#021AB; +entity("larrpl") -> 16#02939; +entity("larrsim") -> 16#02973; +entity("larrtl") -> 16#021A2; +entity("lat") -> 16#02AAB; +entity("latail") -> 16#02919; +entity("late") -> 16#02AAD; +entity("lates") -> [16#02AAD, 16#0FE00]; +entity("lbarr") -> 16#0290C; +entity("lbbrk") -> 16#02772; +entity("lbrace") -> 16#0007B; +entity("lbrack") -> 16#0005B; +entity("lbrke") -> 16#0298B; +entity("lbrksld") -> 16#0298F; +entity("lbrkslu") -> 16#0298D; +entity("lcaron") -> 16#0013E; +entity("lcedil") -> 16#0013C; +entity("lceil") -> 16#02308; +entity("lcub") -> 16#0007B; +entity("lcy") -> 16#0043B; +entity("ldca") -> 16#02936; +entity("ldquo") -> 16#0201C; +entity("ldquor") -> 16#0201E; +entity("ldrdhar") -> 16#02967; +entity("ldrushar") -> 16#0294B; +entity("ldsh") -> 16#021B2; +entity("le") -> 16#02264; +entity("leftarrow") -> 16#02190; +entity("leftarrowtail") -> 16#021A2; +entity("leftharpoondown") -> 16#021BD; +entity("leftharpoonup") -> 16#021BC; +entity("leftleftarrows") -> 16#021C7; +entity("leftrightarrow") -> 16#02194; +entity("leftrightarrows") -> 16#021C6; +entity("leftrightharpoons") -> 16#021CB; +entity("leftrightsquigarrow") -> 16#021AD; +entity("leftthreetimes") -> 16#022CB; +entity("leg") -> 16#022DA; +entity("leq") -> 16#02264; +entity("leqq") -> 16#02266; +entity("leqslant") -> 16#02A7D; +entity("les") -> 16#02A7D; +entity("lescc") -> 16#02AA8; +entity("lesdot") -> 16#02A7F; +entity("lesdoto") -> 16#02A81; +entity("lesdotor") -> 16#02A83; +entity("lesg") -> [16#022DA, 16#0FE00]; +entity("lesges") -> 16#02A93; +entity("lessapprox") -> 16#02A85; +entity("lessdot") -> 16#022D6; +entity("lesseqgtr") -> 16#022DA; +entity("lesseqqgtr") -> 16#02A8B; +entity("lessgtr") -> 16#02276; +entity("lesssim") -> 16#02272; +entity("lfisht") -> 16#0297C; +entity("lfloor") -> 16#0230A; +entity("lfr") -> 16#1D529; +entity("lg") -> 16#02276; +entity("lgE") -> 16#02A91; +entity("lhard") -> 16#021BD; +entity("lharu") -> 16#021BC; +entity("lharul") -> 16#0296A; +entity("lhblk") -> 16#02584; +entity("ljcy") -> 16#00459; +entity("ll") -> 16#0226A; +entity("llarr") -> 16#021C7; +entity("llcorner") -> 16#0231E; +entity("llhard") -> 16#0296B; +entity("lltri") -> 16#025FA; +entity("lmidot") -> 16#00140; +entity("lmoust") -> 16#023B0; +entity("lmoustache") -> 16#023B0; +entity("lnE") -> 16#02268; +entity("lnap") -> 16#02A89; +entity("lnapprox") -> 16#02A89; +entity("lne") -> 16#02A87; +entity("lneq") -> 16#02A87; +entity("lneqq") -> 16#02268; +entity("lnsim") -> 16#022E6; +entity("loang") -> 16#027EC; +entity("loarr") -> 16#021FD; +entity("lobrk") -> 16#027E6; +entity("longleftarrow") -> 16#027F5; +entity("longleftrightarrow") -> 16#027F7; +entity("longmapsto") -> 16#027FC; +entity("longrightarrow") -> 16#027F6; +entity("looparrowleft") -> 16#021AB; +entity("looparrowright") -> 16#021AC; +entity("lopar") -> 16#02985; +entity("lopf") -> 16#1D55D; +entity("loplus") -> 16#02A2D; +entity("lotimes") -> 16#02A34; +entity("lowast") -> 16#02217; +entity("lowbar") -> 16#0005F; +entity("loz") -> 16#025CA; +entity("lozenge") -> 16#025CA; +entity("lozf") -> 16#029EB; +entity("lpar") -> 16#00028; +entity("lparlt") -> 16#02993; +entity("lrarr") -> 16#021C6; +entity("lrcorner") -> 16#0231F; +entity("lrhar") -> 16#021CB; +entity("lrhard") -> 16#0296D; +entity("lrm") -> 16#0200E; +entity("lrtri") -> 16#022BF; +entity("lsaquo") -> 16#02039; +entity("lscr") -> 16#1D4C1; +entity("lsh") -> 16#021B0; +entity("lsim") -> 16#02272; +entity("lsime") -> 16#02A8D; +entity("lsimg") -> 16#02A8F; +entity("lsqb") -> 16#0005B; +entity("lsquo") -> 16#02018; +entity("lsquor") -> 16#0201A; +entity("lstrok") -> 16#00142; +entity("lt") -> 16#0003C; +entity("ltcc") -> 16#02AA6; +entity("ltcir") -> 16#02A79; +entity("ltdot") -> 16#022D6; +entity("lthree") -> 16#022CB; +entity("ltimes") -> 16#022C9; +entity("ltlarr") -> 16#02976; +entity("ltquest") -> 16#02A7B; +entity("ltrPar") -> 16#02996; +entity("ltri") -> 16#025C3; +entity("ltrie") -> 16#022B4; +entity("ltrif") -> 16#025C2; +entity("lurdshar") -> 16#0294A; +entity("luruhar") -> 16#02966; +entity("lvertneqq") -> [16#02268, 16#0FE00]; +entity("lvnE") -> [16#02268, 16#0FE00]; +entity("mDDot") -> 16#0223A; +entity("macr") -> 16#000AF; +entity("male") -> 16#02642; +entity("malt") -> 16#02720; +entity("maltese") -> 16#02720; +entity("map") -> 16#021A6; +entity("mapsto") -> 16#021A6; +entity("mapstodown") -> 16#021A7; +entity("mapstoleft") -> 16#021A4; +entity("mapstoup") -> 16#021A5; +entity("marker") -> 16#025AE; +entity("mcomma") -> 16#02A29; +entity("mcy") -> 16#0043C; +entity("mdash") -> 16#02014; +entity("measuredangle") -> 16#02221; +entity("mfr") -> 16#1D52A; +entity("mho") -> 16#02127; +entity("micro") -> 16#000B5; +entity("mid") -> 16#02223; +entity("midast") -> 16#0002A; +entity("midcir") -> 16#02AF0; +entity("middot") -> 16#000B7; +entity("minus") -> 16#02212; +entity("minusb") -> 16#0229F; +entity("minusd") -> 16#02238; +entity("minusdu") -> 16#02A2A; +entity("mlcp") -> 16#02ADB; +entity("mldr") -> 16#02026; +entity("mnplus") -> 16#02213; +entity("models") -> 16#022A7; +entity("mopf") -> 16#1D55E; +entity("mp") -> 16#02213; +entity("mscr") -> 16#1D4C2; +entity("mstpos") -> 16#0223E; +entity("mu") -> 16#003BC; +entity("multimap") -> 16#022B8; +entity("mumap") -> 16#022B8; +entity("nGg") -> [16#022D9, 16#00338]; +entity("nGt") -> [16#0226B, 16#020D2]; +entity("nGtv") -> [16#0226B, 16#00338]; +entity("nLeftarrow") -> 16#021CD; +entity("nLeftrightarrow") -> 16#021CE; +entity("nLl") -> [16#022D8, 16#00338]; +entity("nLt") -> [16#0226A, 16#020D2]; +entity("nLtv") -> [16#0226A, 16#00338]; +entity("nRightarrow") -> 16#021CF; +entity("nVDash") -> 16#022AF; +entity("nVdash") -> 16#022AE; +entity("nabla") -> 16#02207; +entity("nacute") -> 16#00144; +entity("nang") -> [16#02220, 16#020D2]; +entity("nap") -> 16#02249; +entity("napE") -> [16#02A70, 16#00338]; +entity("napid") -> [16#0224B, 16#00338]; +entity("napos") -> 16#00149; +entity("napprox") -> 16#02249; +entity("natur") -> 16#0266E; +entity("natural") -> 16#0266E; +entity("naturals") -> 16#02115; +entity("nbsp") -> 16#000A0; +entity("nbump") -> [16#0224E, 16#00338]; +entity("nbumpe") -> [16#0224F, 16#00338]; +entity("ncap") -> 16#02A43; +entity("ncaron") -> 16#00148; +entity("ncedil") -> 16#00146; +entity("ncong") -> 16#02247; +entity("ncongdot") -> [16#02A6D, 16#00338]; +entity("ncup") -> 16#02A42; +entity("ncy") -> 16#0043D; +entity("ndash") -> 16#02013; +entity("ne") -> 16#02260; +entity("neArr") -> 16#021D7; +entity("nearhk") -> 16#02924; +entity("nearr") -> 16#02197; +entity("nearrow") -> 16#02197; +entity("nedot") -> [16#02250, 16#00338]; +entity("nequiv") -> 16#02262; +entity("nesear") -> 16#02928; +entity("nesim") -> [16#02242, 16#00338]; +entity("nexist") -> 16#02204; +entity("nexists") -> 16#02204; +entity("nfr") -> 16#1D52B; +entity("ngE") -> [16#02267, 16#00338]; +entity("nge") -> 16#02271; +entity("ngeq") -> 16#02271; +entity("ngeqq") -> [16#02267, 16#00338]; +entity("ngeqslant") -> [16#02A7E, 16#00338]; +entity("nges") -> [16#02A7E, 16#00338]; +entity("ngsim") -> 16#02275; +entity("ngt") -> 16#0226F; +entity("ngtr") -> 16#0226F; +entity("nhArr") -> 16#021CE; +entity("nharr") -> 16#021AE; +entity("nhpar") -> 16#02AF2; +entity("ni") -> 16#0220B; +entity("nis") -> 16#022FC; +entity("nisd") -> 16#022FA; +entity("niv") -> 16#0220B; +entity("njcy") -> 16#0045A; +entity("nlArr") -> 16#021CD; +entity("nlE") -> [16#02266, 16#00338]; +entity("nlarr") -> 16#0219A; +entity("nldr") -> 16#02025; +entity("nle") -> 16#02270; +entity("nleftarrow") -> 16#0219A; +entity("nleftrightarrow") -> 16#021AE; +entity("nleq") -> 16#02270; +entity("nleqq") -> [16#02266, 16#00338]; +entity("nleqslant") -> [16#02A7D, 16#00338]; +entity("nles") -> [16#02A7D, 16#00338]; +entity("nless") -> 16#0226E; +entity("nlsim") -> 16#02274; +entity("nlt") -> 16#0226E; +entity("nltri") -> 16#022EA; +entity("nltrie") -> 16#022EC; +entity("nmid") -> 16#02224; +entity("nopf") -> 16#1D55F; +entity("not") -> 16#000AC; +entity("notin") -> 16#02209; +entity("notinE") -> [16#022F9, 16#00338]; +entity("notindot") -> [16#022F5, 16#00338]; +entity("notinva") -> 16#02209; +entity("notinvb") -> 16#022F7; +entity("notinvc") -> 16#022F6; +entity("notni") -> 16#0220C; +entity("notniva") -> 16#0220C; +entity("notnivb") -> 16#022FE; +entity("notnivc") -> 16#022FD; +entity("npar") -> 16#02226; +entity("nparallel") -> 16#02226; +entity("nparsl") -> [16#02AFD, 16#020E5]; +entity("npart") -> [16#02202, 16#00338]; +entity("npolint") -> 16#02A14; +entity("npr") -> 16#02280; +entity("nprcue") -> 16#022E0; +entity("npre") -> [16#02AAF, 16#00338]; +entity("nprec") -> 16#02280; +entity("npreceq") -> [16#02AAF, 16#00338]; +entity("nrArr") -> 16#021CF; +entity("nrarr") -> 16#0219B; +entity("nrarrc") -> [16#02933, 16#00338]; +entity("nrarrw") -> [16#0219D, 16#00338]; +entity("nrightarrow") -> 16#0219B; +entity("nrtri") -> 16#022EB; +entity("nrtrie") -> 16#022ED; +entity("nsc") -> 16#02281; +entity("nsccue") -> 16#022E1; +entity("nsce") -> [16#02AB0, 16#00338]; +entity("nscr") -> 16#1D4C3; +entity("nshortmid") -> 16#02224; +entity("nshortparallel") -> 16#02226; +entity("nsim") -> 16#02241; +entity("nsime") -> 16#02244; +entity("nsimeq") -> 16#02244; +entity("nsmid") -> 16#02224; +entity("nspar") -> 16#02226; +entity("nsqsube") -> 16#022E2; +entity("nsqsupe") -> 16#022E3; +entity("nsub") -> 16#02284; +entity("nsubE") -> [16#02AC5, 16#00338]; +entity("nsube") -> 16#02288; +entity("nsubset") -> [16#02282, 16#020D2]; +entity("nsubseteq") -> 16#02288; +entity("nsubseteqq") -> [16#02AC5, 16#00338]; +entity("nsucc") -> 16#02281; +entity("nsucceq") -> [16#02AB0, 16#00338]; +entity("nsup") -> 16#02285; +entity("nsupE") -> [16#02AC6, 16#00338]; +entity("nsupe") -> 16#02289; +entity("nsupset") -> [16#02283, 16#020D2]; +entity("nsupseteq") -> 16#02289; +entity("nsupseteqq") -> [16#02AC6, 16#00338]; +entity("ntgl") -> 16#02279; +entity("ntilde") -> 16#000F1; +entity("ntlg") -> 16#02278; +entity("ntriangleleft") -> 16#022EA; +entity("ntrianglelefteq") -> 16#022EC; +entity("ntriangleright") -> 16#022EB; +entity("ntrianglerighteq") -> 16#022ED; +entity("nu") -> 16#003BD; +entity("num") -> 16#00023; +entity("numero") -> 16#02116; +entity("numsp") -> 16#02007; +entity("nvDash") -> 16#022AD; +entity("nvHarr") -> 16#02904; +entity("nvap") -> [16#0224D, 16#020D2]; +entity("nvdash") -> 16#022AC; +entity("nvge") -> [16#02265, 16#020D2]; +entity("nvgt") -> [16#0003E, 16#020D2]; +entity("nvinfin") -> 16#029DE; +entity("nvlArr") -> 16#02902; +entity("nvle") -> [16#02264, 16#020D2]; +entity("nvlt") -> [16#0003C, 16#020D2]; +entity("nvltrie") -> [16#022B4, 16#020D2]; +entity("nvrArr") -> 16#02903; +entity("nvrtrie") -> [16#022B5, 16#020D2]; +entity("nvsim") -> [16#0223C, 16#020D2]; +entity("nwArr") -> 16#021D6; +entity("nwarhk") -> 16#02923; +entity("nwarr") -> 16#02196; +entity("nwarrow") -> 16#02196; +entity("nwnear") -> 16#02927; +entity("oS") -> 16#024C8; +entity("oacute") -> 16#000F3; +entity("oast") -> 16#0229B; +entity("ocir") -> 16#0229A; +entity("ocirc") -> 16#000F4; +entity("ocy") -> 16#0043E; +entity("odash") -> 16#0229D; +entity("odblac") -> 16#00151; +entity("odiv") -> 16#02A38; +entity("odot") -> 16#02299; +entity("odsold") -> 16#029BC; +entity("oelig") -> 16#00153; +entity("ofcir") -> 16#029BF; +entity("ofr") -> 16#1D52C; +entity("ogon") -> 16#002DB; +entity("ograve") -> 16#000F2; +entity("ogt") -> 16#029C1; +entity("ohbar") -> 16#029B5; +entity("ohm") -> 16#003A9; +entity("oint") -> 16#0222E; +entity("olarr") -> 16#021BA; +entity("olcir") -> 16#029BE; +entity("olcross") -> 16#029BB; +entity("oline") -> 16#0203E; +entity("olt") -> 16#029C0; +entity("omacr") -> 16#0014D; +entity("omega") -> 16#003C9; +entity("omicron") -> 16#003BF; +entity("omid") -> 16#029B6; +entity("ominus") -> 16#02296; +entity("oopf") -> 16#1D560; +entity("opar") -> 16#029B7; +entity("operp") -> 16#029B9; +entity("oplus") -> 16#02295; +entity("or") -> 16#02228; +entity("orarr") -> 16#021BB; +entity("ord") -> 16#02A5D; +entity("order") -> 16#02134; +entity("orderof") -> 16#02134; +entity("ordf") -> 16#000AA; +entity("ordm") -> 16#000BA; +entity("origof") -> 16#022B6; +entity("oror") -> 16#02A56; +entity("orslope") -> 16#02A57; +entity("orv") -> 16#02A5B; +entity("oscr") -> 16#02134; +entity("oslash") -> 16#000F8; +entity("osol") -> 16#02298; +entity("otilde") -> 16#000F5; +entity("otimes") -> 16#02297; +entity("otimesas") -> 16#02A36; +entity("ouml") -> 16#000F6; +entity("ovbar") -> 16#0233D; +entity("par") -> 16#02225; +entity("para") -> 16#000B6; +entity("parallel") -> 16#02225; +entity("parsim") -> 16#02AF3; +entity("parsl") -> 16#02AFD; +entity("part") -> 16#02202; +entity("pcy") -> 16#0043F; +entity("percnt") -> 16#00025; +entity("period") -> 16#0002E; +entity("permil") -> 16#02030; +entity("perp") -> 16#022A5; +entity("pertenk") -> 16#02031; +entity("pfr") -> 16#1D52D; +entity("phi") -> 16#003C6; +entity("phiv") -> 16#003D5; +entity("phmmat") -> 16#02133; +entity("phone") -> 16#0260E; +entity("pi") -> 16#003C0; +entity("pitchfork") -> 16#022D4; +entity("piv") -> 16#003D6; +entity("planck") -> 16#0210F; +entity("planckh") -> 16#0210E; +entity("plankv") -> 16#0210F; +entity("plus") -> 16#0002B; +entity("plusacir") -> 16#02A23; +entity("plusb") -> 16#0229E; +entity("pluscir") -> 16#02A22; +entity("plusdo") -> 16#02214; +entity("plusdu") -> 16#02A25; +entity("pluse") -> 16#02A72; +entity("plusmn") -> 16#000B1; +entity("plussim") -> 16#02A26; +entity("plustwo") -> 16#02A27; +entity("pm") -> 16#000B1; +entity("pointint") -> 16#02A15; +entity("popf") -> 16#1D561; +entity("pound") -> 16#000A3; +entity("pr") -> 16#0227A; +entity("prE") -> 16#02AB3; +entity("prap") -> 16#02AB7; +entity("prcue") -> 16#0227C; +entity("pre") -> 16#02AAF; +entity("prec") -> 16#0227A; +entity("precapprox") -> 16#02AB7; +entity("preccurlyeq") -> 16#0227C; +entity("preceq") -> 16#02AAF; +entity("precnapprox") -> 16#02AB9; +entity("precneqq") -> 16#02AB5; +entity("precnsim") -> 16#022E8; +entity("precsim") -> 16#0227E; +entity("prime") -> 16#02032; +entity("primes") -> 16#02119; +entity("prnE") -> 16#02AB5; +entity("prnap") -> 16#02AB9; +entity("prnsim") -> 16#022E8; +entity("prod") -> 16#0220F; +entity("profalar") -> 16#0232E; +entity("profline") -> 16#02312; +entity("profsurf") -> 16#02313; +entity("prop") -> 16#0221D; +entity("propto") -> 16#0221D; +entity("prsim") -> 16#0227E; +entity("prurel") -> 16#022B0; +entity("pscr") -> 16#1D4C5; +entity("psi") -> 16#003C8; +entity("puncsp") -> 16#02008; +entity("qfr") -> 16#1D52E; +entity("qint") -> 16#02A0C; +entity("qopf") -> 16#1D562; +entity("qprime") -> 16#02057; +entity("qscr") -> 16#1D4C6; +entity("quaternions") -> 16#0210D; +entity("quatint") -> 16#02A16; +entity("quest") -> 16#0003F; +entity("questeq") -> 16#0225F; +entity("quot") -> 16#00022; +entity("rAarr") -> 16#021DB; +entity("rArr") -> 16#021D2; +entity("rAtail") -> 16#0291C; +entity("rBarr") -> 16#0290F; +entity("rHar") -> 16#02964; +entity("race") -> [16#0223D, 16#00331]; +entity("racute") -> 16#00155; +entity("radic") -> 16#0221A; +entity("raemptyv") -> 16#029B3; +entity("rang") -> 16#027E9; +entity("rangd") -> 16#02992; +entity("range") -> 16#029A5; +entity("rangle") -> 16#027E9; +entity("raquo") -> 16#000BB; +entity("rarr") -> 16#02192; +entity("rarrap") -> 16#02975; +entity("rarrb") -> 16#021E5; +entity("rarrbfs") -> 16#02920; +entity("rarrc") -> 16#02933; +entity("rarrfs") -> 16#0291E; +entity("rarrhk") -> 16#021AA; +entity("rarrlp") -> 16#021AC; +entity("rarrpl") -> 16#02945; +entity("rarrsim") -> 16#02974; +entity("rarrtl") -> 16#021A3; +entity("rarrw") -> 16#0219D; +entity("ratail") -> 16#0291A; +entity("ratio") -> 16#02236; +entity("rationals") -> 16#0211A; +entity("rbarr") -> 16#0290D; +entity("rbbrk") -> 16#02773; +entity("rbrace") -> 16#0007D; +entity("rbrack") -> 16#0005D; +entity("rbrke") -> 16#0298C; +entity("rbrksld") -> 16#0298E; +entity("rbrkslu") -> 16#02990; +entity("rcaron") -> 16#00159; +entity("rcedil") -> 16#00157; +entity("rceil") -> 16#02309; +entity("rcub") -> 16#0007D; +entity("rcy") -> 16#00440; +entity("rdca") -> 16#02937; +entity("rdldhar") -> 16#02969; +entity("rdquo") -> 16#0201D; +entity("rdquor") -> 16#0201D; +entity("rdsh") -> 16#021B3; +entity("real") -> 16#0211C; +entity("realine") -> 16#0211B; +entity("realpart") -> 16#0211C; +entity("reals") -> 16#0211D; +entity("rect") -> 16#025AD; +entity("reg") -> 16#000AE; +entity("rfisht") -> 16#0297D; +entity("rfloor") -> 16#0230B; +entity("rfr") -> 16#1D52F; +entity("rhard") -> 16#021C1; +entity("rharu") -> 16#021C0; +entity("rharul") -> 16#0296C; +entity("rho") -> 16#003C1; +entity("rhov") -> 16#003F1; +entity("rightarrow") -> 16#02192; +entity("rightarrowtail") -> 16#021A3; +entity("rightharpoondown") -> 16#021C1; +entity("rightharpoonup") -> 16#021C0; +entity("rightleftarrows") -> 16#021C4; +entity("rightleftharpoons") -> 16#021CC; +entity("rightrightarrows") -> 16#021C9; +entity("rightsquigarrow") -> 16#0219D; +entity("rightthreetimes") -> 16#022CC; +entity("ring") -> 16#002DA; +entity("risingdotseq") -> 16#02253; +entity("rlarr") -> 16#021C4; +entity("rlhar") -> 16#021CC; +entity("rlm") -> 16#0200F; +entity("rmoust") -> 16#023B1; +entity("rmoustache") -> 16#023B1; +entity("rnmid") -> 16#02AEE; +entity("roang") -> 16#027ED; +entity("roarr") -> 16#021FE; +entity("robrk") -> 16#027E7; +entity("ropar") -> 16#02986; +entity("ropf") -> 16#1D563; +entity("roplus") -> 16#02A2E; +entity("rotimes") -> 16#02A35; +entity("rpar") -> 16#00029; +entity("rpargt") -> 16#02994; +entity("rppolint") -> 16#02A12; +entity("rrarr") -> 16#021C9; +entity("rsaquo") -> 16#0203A; +entity("rscr") -> 16#1D4C7; +entity("rsh") -> 16#021B1; +entity("rsqb") -> 16#0005D; +entity("rsquo") -> 16#02019; +entity("rsquor") -> 16#02019; +entity("rthree") -> 16#022CC; +entity("rtimes") -> 16#022CA; +entity("rtri") -> 16#025B9; +entity("rtrie") -> 16#022B5; +entity("rtrif") -> 16#025B8; +entity("rtriltri") -> 16#029CE; +entity("ruluhar") -> 16#02968; +entity("rx") -> 16#0211E; +entity("sacute") -> 16#0015B; +entity("sbquo") -> 16#0201A; +entity("sc") -> 16#0227B; +entity("scE") -> 16#02AB4; +entity("scap") -> 16#02AB8; +entity("scaron") -> 16#00161; +entity("sccue") -> 16#0227D; +entity("sce") -> 16#02AB0; +entity("scedil") -> 16#0015F; +entity("scirc") -> 16#0015D; +entity("scnE") -> 16#02AB6; +entity("scnap") -> 16#02ABA; +entity("scnsim") -> 16#022E9; +entity("scpolint") -> 16#02A13; +entity("scsim") -> 16#0227F; +entity("scy") -> 16#00441; +entity("sdot") -> 16#022C5; +entity("sdotb") -> 16#022A1; +entity("sdote") -> 16#02A66; +entity("seArr") -> 16#021D8; +entity("searhk") -> 16#02925; +entity("searr") -> 16#02198; +entity("searrow") -> 16#02198; +entity("sect") -> 16#000A7; +entity("semi") -> 16#0003B; +entity("seswar") -> 16#02929; +entity("setminus") -> 16#02216; +entity("setmn") -> 16#02216; +entity("sext") -> 16#02736; +entity("sfr") -> 16#1D530; +entity("sfrown") -> 16#02322; +entity("sharp") -> 16#0266F; +entity("shchcy") -> 16#00449; +entity("shcy") -> 16#00448; +entity("shortmid") -> 16#02223; +entity("shortparallel") -> 16#02225; +entity("shy") -> 16#000AD; +entity("sigma") -> 16#003C3; +entity("sigmaf") -> 16#003C2; +entity("sigmav") -> 16#003C2; +entity("sim") -> 16#0223C; +entity("simdot") -> 16#02A6A; +entity("sime") -> 16#02243; +entity("simeq") -> 16#02243; +entity("simg") -> 16#02A9E; +entity("simgE") -> 16#02AA0; +entity("siml") -> 16#02A9D; +entity("simlE") -> 16#02A9F; +entity("simne") -> 16#02246; +entity("simplus") -> 16#02A24; +entity("simrarr") -> 16#02972; +entity("slarr") -> 16#02190; +entity("smallsetminus") -> 16#02216; +entity("smashp") -> 16#02A33; +entity("smeparsl") -> 16#029E4; +entity("smid") -> 16#02223; +entity("smile") -> 16#02323; +entity("smt") -> 16#02AAA; +entity("smte") -> 16#02AAC; +entity("smtes") -> [16#02AAC, 16#0FE00]; +entity("softcy") -> 16#0044C; +entity("sol") -> 16#0002F; +entity("solb") -> 16#029C4; +entity("solbar") -> 16#0233F; +entity("sopf") -> 16#1D564; +entity("spades") -> 16#02660; +entity("spadesuit") -> 16#02660; +entity("spar") -> 16#02225; +entity("sqcap") -> 16#02293; +entity("sqcaps") -> [16#02293, 16#0FE00]; +entity("sqcup") -> 16#02294; +entity("sqcups") -> [16#02294, 16#0FE00]; +entity("sqsub") -> 16#0228F; +entity("sqsube") -> 16#02291; +entity("sqsubset") -> 16#0228F; +entity("sqsubseteq") -> 16#02291; +entity("sqsup") -> 16#02290; +entity("sqsupe") -> 16#02292; +entity("sqsupset") -> 16#02290; +entity("sqsupseteq") -> 16#02292; +entity("squ") -> 16#025A1; +entity("square") -> 16#025A1; +entity("squarf") -> 16#025AA; +entity("squf") -> 16#025AA; +entity("srarr") -> 16#02192; +entity("sscr") -> 16#1D4C8; +entity("ssetmn") -> 16#02216; +entity("ssmile") -> 16#02323; +entity("sstarf") -> 16#022C6; +entity("star") -> 16#02606; +entity("starf") -> 16#02605; +entity("straightepsilon") -> 16#003F5; +entity("straightphi") -> 16#003D5; +entity("strns") -> 16#000AF; +entity("sub") -> 16#02282; +entity("subE") -> 16#02AC5; +entity("subdot") -> 16#02ABD; +entity("sube") -> 16#02286; +entity("subedot") -> 16#02AC3; +entity("submult") -> 16#02AC1; +entity("subnE") -> 16#02ACB; +entity("subne") -> 16#0228A; +entity("subplus") -> 16#02ABF; +entity("subrarr") -> 16#02979; +entity("subset") -> 16#02282; +entity("subseteq") -> 16#02286; +entity("subseteqq") -> 16#02AC5; +entity("subsetneq") -> 16#0228A; +entity("subsetneqq") -> 16#02ACB; +entity("subsim") -> 16#02AC7; +entity("subsub") -> 16#02AD5; +entity("subsup") -> 16#02AD3; +entity("succ") -> 16#0227B; +entity("succapprox") -> 16#02AB8; +entity("succcurlyeq") -> 16#0227D; +entity("succeq") -> 16#02AB0; +entity("succnapprox") -> 16#02ABA; +entity("succneqq") -> 16#02AB6; +entity("succnsim") -> 16#022E9; +entity("succsim") -> 16#0227F; +entity("sum") -> 16#02211; +entity("sung") -> 16#0266A; +entity("sup") -> 16#02283; +entity("sup1") -> 16#000B9; +entity("sup2") -> 16#000B2; +entity("sup3") -> 16#000B3; +entity("supE") -> 16#02AC6; +entity("supdot") -> 16#02ABE; +entity("supdsub") -> 16#02AD8; +entity("supe") -> 16#02287; +entity("supedot") -> 16#02AC4; +entity("suphsol") -> 16#027C9; +entity("suphsub") -> 16#02AD7; +entity("suplarr") -> 16#0297B; +entity("supmult") -> 16#02AC2; +entity("supnE") -> 16#02ACC; +entity("supne") -> 16#0228B; +entity("supplus") -> 16#02AC0; +entity("supset") -> 16#02283; +entity("supseteq") -> 16#02287; +entity("supseteqq") -> 16#02AC6; +entity("supsetneq") -> 16#0228B; +entity("supsetneqq") -> 16#02ACC; +entity("supsim") -> 16#02AC8; +entity("supsub") -> 16#02AD4; +entity("supsup") -> 16#02AD6; +entity("swArr") -> 16#021D9; +entity("swarhk") -> 16#02926; +entity("swarr") -> 16#02199; +entity("swarrow") -> 16#02199; +entity("swnwar") -> 16#0292A; +entity("szlig") -> 16#000DF; +entity("target") -> 16#02316; +entity("tau") -> 16#003C4; +entity("tbrk") -> 16#023B4; +entity("tcaron") -> 16#00165; +entity("tcedil") -> 16#00163; +entity("tcy") -> 16#00442; +entity("tdot") -> 16#020DB; +entity("telrec") -> 16#02315; +entity("tfr") -> 16#1D531; +entity("there4") -> 16#02234; +entity("therefore") -> 16#02234; +entity("theta") -> 16#003B8; +entity("thetasym") -> 16#003D1; +entity("thetav") -> 16#003D1; +entity("thickapprox") -> 16#02248; +entity("thicksim") -> 16#0223C; +entity("thinsp") -> 16#02009; +entity("thkap") -> 16#02248; +entity("thksim") -> 16#0223C; +entity("thorn") -> 16#000FE; +entity("tilde") -> 16#002DC; +entity("times") -> 16#000D7; +entity("timesb") -> 16#022A0; +entity("timesbar") -> 16#02A31; +entity("timesd") -> 16#02A30; +entity("tint") -> 16#0222D; +entity("toea") -> 16#02928; +entity("top") -> 16#022A4; +entity("topbot") -> 16#02336; +entity("topcir") -> 16#02AF1; +entity("topf") -> 16#1D565; +entity("topfork") -> 16#02ADA; +entity("tosa") -> 16#02929; +entity("tprime") -> 16#02034; +entity("trade") -> 16#02122; +entity("triangle") -> 16#025B5; +entity("triangledown") -> 16#025BF; +entity("triangleleft") -> 16#025C3; +entity("trianglelefteq") -> 16#022B4; +entity("triangleq") -> 16#0225C; +entity("triangleright") -> 16#025B9; +entity("trianglerighteq") -> 16#022B5; +entity("tridot") -> 16#025EC; +entity("trie") -> 16#0225C; +entity("triminus") -> 16#02A3A; +entity("triplus") -> 16#02A39; +entity("trisb") -> 16#029CD; +entity("tritime") -> 16#02A3B; +entity("trpezium") -> 16#023E2; +entity("tscr") -> 16#1D4C9; +entity("tscy") -> 16#00446; +entity("tshcy") -> 16#0045B; +entity("tstrok") -> 16#00167; +entity("twixt") -> 16#0226C; +entity("twoheadleftarrow") -> 16#0219E; +entity("twoheadrightarrow") -> 16#021A0; +entity("uArr") -> 16#021D1; +entity("uHar") -> 16#02963; +entity("uacute") -> 16#000FA; +entity("uarr") -> 16#02191; +entity("ubrcy") -> 16#0045E; +entity("ubreve") -> 16#0016D; +entity("ucirc") -> 16#000FB; +entity("ucy") -> 16#00443; +entity("udarr") -> 16#021C5; +entity("udblac") -> 16#00171; +entity("udhar") -> 16#0296E; +entity("ufisht") -> 16#0297E; +entity("ufr") -> 16#1D532; +entity("ugrave") -> 16#000F9; +entity("uharl") -> 16#021BF; +entity("uharr") -> 16#021BE; +entity("uhblk") -> 16#02580; +entity("ulcorn") -> 16#0231C; +entity("ulcorner") -> 16#0231C; +entity("ulcrop") -> 16#0230F; +entity("ultri") -> 16#025F8; +entity("umacr") -> 16#0016B; +entity("uml") -> 16#000A8; +entity("uogon") -> 16#00173; +entity("uopf") -> 16#1D566; +entity("uparrow") -> 16#02191; +entity("updownarrow") -> 16#02195; +entity("upharpoonleft") -> 16#021BF; +entity("upharpoonright") -> 16#021BE; +entity("uplus") -> 16#0228E; +entity("upsi") -> 16#003C5; +entity("upsih") -> 16#003D2; +entity("upsilon") -> 16#003C5; +entity("upuparrows") -> 16#021C8; +entity("urcorn") -> 16#0231D; +entity("urcorner") -> 16#0231D; +entity("urcrop") -> 16#0230E; +entity("uring") -> 16#0016F; +entity("urtri") -> 16#025F9; +entity("uscr") -> 16#1D4CA; +entity("utdot") -> 16#022F0; +entity("utilde") -> 16#00169; +entity("utri") -> 16#025B5; +entity("utrif") -> 16#025B4; +entity("uuarr") -> 16#021C8; +entity("uuml") -> 16#000FC; +entity("uwangle") -> 16#029A7; +entity("vArr") -> 16#021D5; +entity("vBar") -> 16#02AE8; +entity("vBarv") -> 16#02AE9; +entity("vDash") -> 16#022A8; +entity("vangrt") -> 16#0299C; +entity("varepsilon") -> 16#003F5; +entity("varkappa") -> 16#003F0; +entity("varnothing") -> 16#02205; +entity("varphi") -> 16#003D5; +entity("varpi") -> 16#003D6; +entity("varpropto") -> 16#0221D; +entity("varr") -> 16#02195; +entity("varrho") -> 16#003F1; +entity("varsigma") -> 16#003C2; +entity("varsubsetneq") -> [16#0228A, 16#0FE00]; +entity("varsubsetneqq") -> [16#02ACB, 16#0FE00]; +entity("varsupsetneq") -> [16#0228B, 16#0FE00]; +entity("varsupsetneqq") -> [16#02ACC, 16#0FE00]; +entity("vartheta") -> 16#003D1; +entity("vartriangleleft") -> 16#022B2; +entity("vartriangleright") -> 16#022B3; +entity("vcy") -> 16#00432; +entity("vdash") -> 16#022A2; +entity("vee") -> 16#02228; +entity("veebar") -> 16#022BB; +entity("veeeq") -> 16#0225A; +entity("vellip") -> 16#022EE; +entity("verbar") -> 16#0007C; +entity("vert") -> 16#0007C; +entity("vfr") -> 16#1D533; +entity("vltri") -> 16#022B2; +entity("vnsub") -> [16#02282, 16#020D2]; +entity("vnsup") -> [16#02283, 16#020D2]; +entity("vopf") -> 16#1D567; +entity("vprop") -> 16#0221D; +entity("vrtri") -> 16#022B3; +entity("vscr") -> 16#1D4CB; +entity("vsubnE") -> [16#02ACB, 16#0FE00]; +entity("vsubne") -> [16#0228A, 16#0FE00]; +entity("vsupnE") -> [16#02ACC, 16#0FE00]; +entity("vsupne") -> [16#0228B, 16#0FE00]; +entity("vzigzag") -> 16#0299A; +entity("wcirc") -> 16#00175; +entity("wedbar") -> 16#02A5F; +entity("wedge") -> 16#02227; +entity("wedgeq") -> 16#02259; +entity("weierp") -> 16#02118; +entity("wfr") -> 16#1D534; +entity("wopf") -> 16#1D568; +entity("wp") -> 16#02118; +entity("wr") -> 16#02240; +entity("wreath") -> 16#02240; +entity("wscr") -> 16#1D4CC; +entity("xcap") -> 16#022C2; +entity("xcirc") -> 16#025EF; +entity("xcup") -> 16#022C3; +entity("xdtri") -> 16#025BD; +entity("xfr") -> 16#1D535; +entity("xhArr") -> 16#027FA; +entity("xharr") -> 16#027F7; +entity("xi") -> 16#003BE; +entity("xlArr") -> 16#027F8; +entity("xlarr") -> 16#027F5; +entity("xmap") -> 16#027FC; +entity("xnis") -> 16#022FB; +entity("xodot") -> 16#02A00; +entity("xopf") -> 16#1D569; +entity("xoplus") -> 16#02A01; +entity("xotime") -> 16#02A02; +entity("xrArr") -> 16#027F9; +entity("xrarr") -> 16#027F6; +entity("xscr") -> 16#1D4CD; +entity("xsqcup") -> 16#02A06; +entity("xuplus") -> 16#02A04; +entity("xutri") -> 16#025B3; +entity("xvee") -> 16#022C1; +entity("xwedge") -> 16#022C0; +entity("yacute") -> 16#000FD; +entity("yacy") -> 16#0044F; +entity("ycirc") -> 16#00177; +entity("ycy") -> 16#0044B; +entity("yen") -> 16#000A5; +entity("yfr") -> 16#1D536; +entity("yicy") -> 16#00457; +entity("yopf") -> 16#1D56A; +entity("yscr") -> 16#1D4CE; +entity("yucy") -> 16#0044E; +entity("yuml") -> 16#000FF; +entity("zacute") -> 16#0017A; +entity("zcaron") -> 16#0017E; +entity("zcy") -> 16#00437; +entity("zdot") -> 16#0017C; +entity("zeetrf") -> 16#02128; +entity("zeta") -> 16#003B6; +entity("zfr") -> 16#1D537; +entity("zhcy") -> 16#00436; +entity("zigrarr") -> 16#021DD; +entity("zopf") -> 16#1D56B; +entity("zscr") -> 16#1D4CF; +entity("zwj") -> 16#0200D; +entity("zwnj") -> 16#0200C; +entity(_) -> undefined. %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). exhaustive_entity_test() -> T = mochiweb_cover:clause_lookup_table(?MODULE, entity), diff --git a/src/mochiweb/mochiweb_cookies.erl b/src/mochiweb/mochiweb_cookies.erl index ee91d0c1d..1cc4e91f3 100644 --- a/src/mochiweb/mochiweb_cookies.erl +++ b/src/mochiweb/mochiweb_cookies.erl @@ -23,6 +23,7 @@ %% @type proplist() = [{Key::string(), Value::string()}]. %% @type header() = {Name::string(), Value::string()}. +%% @type int_seconds() = integer(). %% @spec cookie(Key::string(), Value::string()) -> header() %% @doc Short-hand for <code>cookie(Key, Value, [])</code>. @@ -30,7 +31,7 @@ cookie(Key, Value) -> cookie(Key, Value, []). %% @spec cookie(Key::string(), Value::string(), Options::[Option]) -> header() -%% where Option = {max_age, integer()} | {local_time, {date(), time()}} +%% where Option = {max_age, int_seconds()} | {local_time, {date(), time()}} %% | {domain, string()} | {path, string()} %% | {secure, true | false} | {http_only, true | false} %% @@ -49,9 +50,9 @@ cookie(Key, Value, Options) -> RawAge -> When = case proplists:get_value(local_time, Options) of undefined -> - calendar:universal_time(); + calendar:local_time(); LocalTime -> - erlang:localtime_to_universaltime(LocalTime) + LocalTime end, Age = case RawAge < 0 of true -> @@ -115,12 +116,33 @@ quote(V0) -> orelse erlang:error({cookie_quoting_required, V}), V. -add_seconds(Secs, UniversalTime) -> - Greg = calendar:datetime_to_gregorian_seconds(UniversalTime), + +%% Return a date in the form of: Wdy, DD-Mon-YYYY HH:MM:SS GMT +%% See also: rfc2109: 10.1.2 +rfc2109_cookie_expires_date(LocalTime) -> + {{YYYY,MM,DD},{Hour,Min,Sec}} = + case calendar:local_time_to_universal_time_dst(LocalTime) of + [] -> + {Date, {Hour1, Min1, Sec1}} = LocalTime, + LocalTime2 = {Date, {Hour1 + 1, Min1, Sec1}}, + case calendar:local_time_to_universal_time_dst(LocalTime2) of + [Gmt] -> Gmt; + [_,Gmt] -> Gmt + end; + [Gmt] -> Gmt; + [_,Gmt] -> Gmt + end, + DayNumber = calendar:day_of_the_week({YYYY,MM,DD}), + lists:flatten( + io_lib:format("~s, ~2.2.0w-~3.s-~4.4.0w ~2.2.0w:~2.2.0w:~2.2.0w GMT", + [httpd_util:day(DayNumber),DD,httpd_util:month(MM),YYYY,Hour,Min,Sec])). + +add_seconds(Secs, LocalTime) -> + Greg = calendar:datetime_to_gregorian_seconds(LocalTime), calendar:gregorian_seconds_to_datetime(Greg + Secs). -age_to_cookie_date(Age, UniversalTime) -> - couch_util:rfc1123_date(add_seconds(Age, UniversalTime)). +age_to_cookie_date(Age, LocalTime) -> + rfc2109_cookie_expires_date(add_seconds(Age, LocalTime)). %% @spec parse_cookie(string()) -> [{K::string(), V::string()}] %% @doc Parse the contents of a Cookie header field, ignoring cookie @@ -203,8 +225,8 @@ any_to_list(V) when is_integer(V) -> %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). quote_test() -> %% ?assertError eunit macro is not compatible with coverage module @@ -293,14 +315,14 @@ cookie_test() -> C2 = {"Set-Cookie", "Customer=WILE_E_COYOTE; " "Version=1; " - "Expires=Tue, 15 May 2007 13:45:33 GMT; " + "Expires=Tue, 15-May-2007 13:45:33 GMT; " "Max-Age=0"}, C2 = cookie("Customer", "WILE_E_COYOTE", [{max_age, -111}, {local_time, LocalTime}]), C3 = {"Set-Cookie", "Customer=WILE_E_COYOTE; " "Version=1; " - "Expires=Wed, 16 May 2007 13:45:50 GMT; " + "Expires=Wed, 16-May-2007 13:45:50 GMT; " "Max-Age=86417"}, C3 = cookie("Customer", "WILE_E_COYOTE", [{max_age, 86417}, {local_time, LocalTime}]), diff --git a/src/mochiweb/mochiweb_cover.erl b/src/mochiweb/mochiweb_cover.erl index 6a14ef51a..aa075d5bc 100644 --- a/src/mochiweb/mochiweb_cover.erl +++ b/src/mochiweb/mochiweb_cover.erl @@ -46,8 +46,8 @@ clause_fold(_, Acc) -> %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). foo_table(a) -> b; foo_table("a") -> <<"b">>; foo_table(123) -> {4, 3, 2}; diff --git a/src/mochiweb/mochiweb_echo.erl b/src/mochiweb/mochiweb_echo.erl index 6f7872b97..e145840a4 100644 --- a/src/mochiweb/mochiweb_echo.erl +++ b/src/mochiweb/mochiweb_echo.erl @@ -11,11 +11,14 @@ stop() -> mochiweb_socket_server:stop(?MODULE). start() -> - mochiweb_socket_server:start([{name, ?MODULE}, - {port, 6789}, - {ip, "127.0.0.1"}, - {max, 1}, - {loop, {?MODULE, loop}}]). + mochiweb_socket_server:start([{link, false} | options()]). + +options() -> + [{name, ?MODULE}, + {port, 6789}, + {ip, "127.0.0.1"}, + {max, 1}, + {loop, {?MODULE, loop}}]. loop(Socket) -> case mochiweb_socket:recv(Socket, 0, 30000) of @@ -33,6 +36,6 @@ loop(Socket) -> %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). -endif. diff --git a/src/mochiweb/mochiweb_headers.erl b/src/mochiweb/mochiweb_headers.erl index 4fce9838b..b49cf9e7b 100644 --- a/src/mochiweb/mochiweb_headers.erl +++ b/src/mochiweb/mochiweb_headers.erl @@ -6,7 +6,7 @@ -module(mochiweb_headers). -author('bob@mochimedia.com'). -export([empty/0, from_list/1, insert/3, enter/3, get_value/2, lookup/2]). --export([delete_any/2, get_primary_value/2]). +-export([delete_any/2, get_primary_value/2, get_combined_value/2]). -export([default/3, enter_from_list/2, default_from_list/2]). -export([to_list/1, make/1]). -export([from_binary/1]). @@ -24,8 +24,8 @@ empty() -> %% @doc Construct a headers() from the given list. make(L) when is_list(L) -> from_list(L); -%% assume a tuple is already mochiweb_headers. -make(T) when is_tuple(T) -> +%% assume a non-list is already mochiweb_headers. +make(T) -> T. %% @spec from_binary(iolist()) -> headers() @@ -112,6 +112,34 @@ get_primary_value(K, T) -> lists:takewhile(fun (C) -> C =/= $; end, V) end. +%% @spec get_combined_value(key(), headers()) -> string() | undefined +%% @doc Return the value from the given header using a case insensitive search. +%% If the value of the header is a comma-separated list where holds values +%% are all identical, the identical value will be returned. +%% undefined will be returned for keys that are not present or the +%% values in the list are not the same. +%% +%% NOTE: The process isn't designed for a general purpose. If you need +%% to access all values in the combined header, please refer to +%% '''tokenize_header_value/1'''. +%% +%% Section 4.2 of the RFC 2616 (HTTP 1.1) describes multiple message-header +%% fields with the same field-name may be present in a message if and only +%% if the entire field-value for that header field is defined as a +%% comma-separated list [i.e., #(values)]. +get_combined_value(K, T) -> + case get_value(K, T) of + undefined -> + undefined; + V -> + case sets:to_list(sets:from_list(tokenize_header_value(V))) of + [Val] -> + Val; + _ -> + undefined + end + end. + %% @spec lookup(key(), headers()) -> {value, {key(), string()}} | none %% @doc Return the case preserved key and value for the given header using %% a case insensitive search. none will be returned for keys that are @@ -164,6 +192,49 @@ delete_any(K, T) -> %% Internal API +tokenize_header_value(undefined) -> + undefined; +tokenize_header_value(V) -> + reversed_tokens(trim_and_reverse(V, false), [], []). + +trim_and_reverse([S | Rest], Reversed) when S=:=$ ; S=:=$\n; S=:=$\t -> + trim_and_reverse(Rest, Reversed); +trim_and_reverse(V, false) -> + trim_and_reverse(lists:reverse(V), true); +trim_and_reverse(V, true) -> + V. + +reversed_tokens([], [], Acc) -> + Acc; +reversed_tokens([], Token, Acc) -> + [Token | Acc]; +reversed_tokens("\"" ++ Rest, [], Acc) -> + case extract_quoted_string(Rest, []) of + {String, NewRest} -> + reversed_tokens(NewRest, [], [String | Acc]); + undefined -> + undefined + end; +reversed_tokens("\"" ++ _Rest, _Token, _Acc) -> + undefined; +reversed_tokens([C | Rest], [], Acc) when C=:=$ ;C=:=$\n;C=:=$\t;C=:=$, -> + reversed_tokens(Rest, [], Acc); +reversed_tokens([C | Rest], Token, Acc) when C=:=$ ;C=:=$\n;C=:=$\t;C=:=$, -> + reversed_tokens(Rest, [], [Token | Acc]); +reversed_tokens([C | Rest], Token, Acc) -> + reversed_tokens(Rest, [C | Token], Acc); +reversed_tokens(_, _, _) -> + undefeined. + +extract_quoted_string([], _Acc) -> + undefined; +extract_quoted_string("\"\\" ++ Rest, Acc) -> + extract_quoted_string(Rest, "\"" ++ Acc); +extract_quoted_string("\"" ++ Rest, Acc) -> + {Acc, Rest}; +extract_quoted_string([C | Rest], Acc) -> + extract_quoted_string(Rest, [C | Acc]). + expand({array, L}) -> mochiweb_util:join(lists:reverse(L), ", "); expand(V) -> @@ -195,8 +266,8 @@ any_to_list(V) when is_integer(V) -> %% %% Tests. %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). make_test() -> Identity = make([{hdr, foo}]), @@ -237,6 +308,37 @@ get_primary_value_test() -> get_primary_value(<<"baz">>, H)), ok. +get_combined_value_test() -> + H = make([{hdr, foo}, {baz, <<"wibble,taco">>}, {content_length, "123, 123"}, + {test, " 123, 123, 123 , 123,123 "}, + {test2, "456, 123, 123 , 123"}, + {test3, "123"}, {test4, " 123, "}]), + ?assertEqual( + "foo", + get_combined_value(hdr, H)), + ?assertEqual( + undefined, + get_combined_value(bar, H)), + ?assertEqual( + undefined, + get_combined_value(<<"baz">>, H)), + ?assertEqual( + "123", + get_combined_value(<<"content_length">>, H)), + ?assertEqual( + "123", + get_combined_value(<<"test">>, H)), + ?assertEqual( + undefined, + get_combined_value(<<"test2">>, H)), + ?assertEqual( + "123", + get_combined_value(<<"test3">>, H)), + ?assertEqual( + "123", + get_combined_value(<<"test4">>, H)), + ok. + set_cookie_test() -> H = make([{"set-cookie", foo}, {"set-cookie", bar}, {"set-cookie", baz}]), ?assertEqual( @@ -296,4 +398,23 @@ headers_test() -> [] = ?MODULE:to_list(?MODULE:from_binary([<<"\r\n\r\n">>])), ok. +tokenize_header_value_test() -> + ?assertEqual(["a quote in a \"quote\"."], + tokenize_header_value("\"a quote in a \\\"quote\\\".\"")), + ?assertEqual(["abc"], tokenize_header_value("abc")), + ?assertEqual(["abc", "def"], tokenize_header_value("abc def")), + ?assertEqual(["abc", "def"], tokenize_header_value("abc , def")), + ?assertEqual(["abc", "def"], tokenize_header_value(",abc ,, def,,")), + ?assertEqual(["abc def"], tokenize_header_value("\"abc def\" ")), + ?assertEqual(["abc, def"], tokenize_header_value("\"abc, def\"")), + ?assertEqual(["\\a\\$"], tokenize_header_value("\"\\a\\$\"")), + ?assertEqual(["abc def", "foo, bar", "12345", ""], + tokenize_header_value("\"abc def\" \"foo, bar\" , 12345, \"\"")), + ?assertEqual(undefined, + tokenize_header_value(undefined)), + ?assertEqual(undefined, + tokenize_header_value("umatched quote\"")), + ?assertEqual(undefined, + tokenize_header_value("\"unmatched quote")). + -endif. diff --git a/src/mochiweb/mochiweb_html.erl b/src/mochiweb/mochiweb_html.erl index 0f281db95..965c846eb 100644 --- a/src/mochiweb/mochiweb_html.erl +++ b/src/mochiweb/mochiweb_html.erl @@ -95,7 +95,12 @@ to_tokens({Tag0, Acc}) -> to_tokens({Tag0, [], Acc}); to_tokens({Tag0, Attrs, Acc}) -> Tag = to_tag(Tag0), - to_tokens([{Tag, Acc}], [{start_tag, Tag, Attrs, is_singleton(Tag)}]). + case is_singleton(Tag) of + true -> + to_tokens([], [{start_tag, Tag, Attrs, true}]); + false -> + to_tokens([{Tag, Acc}], [{start_tag, Tag, Attrs, false}]) + end. %% @spec to_html([html_token()] | html_node()) -> iolist() %% @doc Convert a list of html_token() to a HTML document. @@ -312,7 +317,8 @@ tokenize(B, S=#decoder{offset=O}) -> {Tag, S1} = tokenize_literal(B, ?ADV_COL(S, 2)), {S2, _} = find_gt(B, S1), {{end_tag, Tag}, S2}; - <<_:O/binary, "<", C, _/binary>> when ?IS_WHITESPACE(C) -> + <<_:O/binary, "<", C, _/binary>> + when ?IS_WHITESPACE(C); not ?IS_LITERAL_SAFE(C) -> %% This isn't really strict HTML {{data, Data, _Whitespace}, S1} = tokenize_data(B, ?INC_COL(S)), {{data, <<$<, Data/binary>>, false}, S1}; @@ -480,7 +486,7 @@ tokenize_attr_value(Attr, B, S) -> _ -> {Attr, S1} end. - + tokenize_quoted_or_unquoted_attr_value(B, S=#decoder{offset=O}) -> case B of <<_:O/binary>> -> @@ -491,7 +497,7 @@ tokenize_quoted_or_unquoted_attr_value(B, S=#decoder{offset=O}) -> <<_:O/binary, _/binary>> -> tokenize_unquoted_attr_value(B, S, []) end. - + tokenize_quoted_attr_value(B, S=#decoder{offset=O}, Acc, Q) -> case B of <<_:O/binary>> -> @@ -501,12 +507,10 @@ tokenize_quoted_attr_value(B, S=#decoder{offset=O}, Acc, Q) -> tokenize_quoted_attr_value(B, S1, [Data|Acc], Q); <<_:O/binary, Q, _/binary>> -> { iolist_to_binary(lists:reverse(Acc)), ?INC_COL(S) }; - <<_:O/binary, $\n, _/binary>> -> - { iolist_to_binary(lists:reverse(Acc)), ?INC_LINE(S) }; <<_:O/binary, C, _/binary>> -> tokenize_quoted_attr_value(B, ?INC_COL(S), [C|Acc], Q) end. - + tokenize_unquoted_attr_value(B, S=#decoder{offset=O}, Acc) -> case B of <<_:O/binary>> -> @@ -520,7 +524,7 @@ tokenize_unquoted_attr_value(B, S=#decoder{offset=O}, Acc) -> { iolist_to_binary(lists:reverse(Acc)), S }; <<_:O/binary, C, _/binary>> -> tokenize_unquoted_attr_value(B, ?INC_COL(S), [C|Acc]) - end. + end. skip_whitespace(B, S=#decoder{offset=O}) -> case B of @@ -603,32 +607,33 @@ find_gt(Bin, S=#decoder{offset=O}, HasSlash) -> end. tokenize_charref(Bin, S=#decoder{offset=O}) -> - tokenize_charref(Bin, S, O). + try + tokenize_charref(Bin, S, O) + catch + throw:invalid_charref -> + {{data, <<"&">>, false}, S} + end. tokenize_charref(Bin, S=#decoder{offset=O}, Start) -> case Bin of <<_:O/binary>> -> - <<_:Start/binary, Raw/binary>> = Bin, - {{data, Raw, false}, S}; + throw(invalid_charref); <<_:O/binary, C, _/binary>> when ?IS_WHITESPACE(C) orelse C =:= ?SQUOTE orelse C =:= ?QUOTE orelse C =:= $/ orelse C =:= $> -> - Len = O - Start, - <<_:Start/binary, Raw:Len/binary, _/binary>> = Bin, - {{data, Raw, false}, S}; + throw(invalid_charref); <<_:O/binary, $;, _/binary>> -> Len = O - Start, <<_:Start/binary, Raw:Len/binary, _/binary>> = Bin, Data = case mochiweb_charref:charref(Raw) of undefined -> - Start1 = Start - 1, - Len1 = Len + 2, - <<_:Start1/binary, R:Len1/binary, _/binary>> = Bin, - R; - Unichar -> - mochiutf8:codepoint_to_bytes(Unichar) + throw(invalid_charref); + Unichar when is_integer(Unichar) -> + mochiutf8:codepoint_to_bytes(Unichar); + Unichars when is_list(Unichars) -> + unicode:characters_to_binary(Unichars) end, {{data, Data, false}, ?INC_COL(S)}; _ -> @@ -759,8 +764,8 @@ tokenize_textarea(Bin, S=#decoder{offset=O}, Start) -> %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). to_html_test() -> ?assertEqual( @@ -1195,43 +1200,51 @@ parse_unquoted_attr_test() -> { <<"img">>, [ { <<"src">>, <<"/images/icon.png">> } ], [] } ]}, mochiweb_html:parse(D0)), - + D1 = <<"<html><img src=/images/icon.png></img></html>">>, ?assertEqual( {<<"html">>,[],[ { <<"img">>, [ { <<"src">>, <<"/images/icon.png">> } ], [] } ]}, mochiweb_html:parse(D1)), - + D2 = <<"<html><img src=/images/icon>.png width=100></img></html>">>, ?assertEqual( {<<"html">>,[],[ { <<"img">>, [ { <<"src">>, <<"/images/icon>.png">> }, { <<"width">>, <<"100">> } ], [] } ]}, mochiweb_html:parse(D2)), - ok. - -parse_quoted_attr_test() -> + ok. + +parse_quoted_attr_test() -> D0 = <<"<html><img src='/images/icon.png'></html>">>, ?assertEqual( {<<"html">>,[],[ { <<"img">>, [ { <<"src">>, <<"/images/icon.png">> } ], [] } ]}, - mochiweb_html:parse(D0)), - + mochiweb_html:parse(D0)), + D1 = <<"<html><img src=\"/images/icon.png'></html>">>, ?assertEqual( {<<"html">>,[],[ { <<"img">>, [ { <<"src">>, <<"/images/icon.png'></html>">> } ], [] } ]}, - mochiweb_html:parse(D1)), + mochiweb_html:parse(D1)), D2 = <<"<html><img src=\"/images/icon>.png\"></html>">>, ?assertEqual( {<<"html">>,[],[ { <<"img">>, [ { <<"src">>, <<"/images/icon>.png">> } ], [] } ]}, - mochiweb_html:parse(D2)), + mochiweb_html:parse(D2)), + + %% Quoted attributes can contain whitespace and newlines + D3 = <<"<html><a href=\"#\" onclick=\"javascript: test(1,\ntrue);\"></html>">>, + ?assertEqual( + {<<"html">>,[],[ + { <<"a">>, [ { <<"href">>, <<"#">> }, {<<"onclick">>, <<"javascript: test(1,\ntrue);">>} ], [] } + ]}, + mochiweb_html:parse(D3)), ok. parse_missing_attr_name_test() -> @@ -1245,7 +1258,7 @@ parse_broken_pi_test() -> D0 = <<"<html><?xml:namespace prefix = o ns = \"urn:schemas-microsoft-com:office:office\" /></html>">>, ?assertEqual( {<<"html">>, [], [ - { pi, <<"xml:namespace">>, [ { <<"prefix">>, <<"o">> }, + { pi, <<"xml:namespace">>, [ { <<"prefix">>, <<"o">> }, { <<"ns">>, <<"urn:schemas-microsoft-com:office:office">> } ] } ] }, mochiweb_html:parse(D0)), @@ -1260,5 +1273,60 @@ parse_funny_singletons_test() -> ] }, mochiweb_html:parse(D0)), ok. - + +to_html_singleton_test() -> + D0 = <<"<link />">>, + T0 = {<<"link">>,[],[]}, + ?assertEqual(D0, iolist_to_binary(to_html(T0))), + + D1 = <<"<head><link /></head>">>, + T1 = {<<"head">>,[],[{<<"link">>,[],[]}]}, + ?assertEqual(D1, iolist_to_binary(to_html(T1))), + + D2 = <<"<head><link /><link /></head>">>, + T2 = {<<"head">>,[],[{<<"link">>,[],[]}, {<<"link">>,[],[]}]}, + ?assertEqual(D2, iolist_to_binary(to_html(T2))), + + %% Make sure singletons are converted to singletons. + D3 = <<"<head><link /></head>">>, + T3 = {<<"head">>,[],[{<<"link">>,[],[<<"funny">>]}]}, + ?assertEqual(D3, iolist_to_binary(to_html(T3))), + + D4 = <<"<link />">>, + T4 = {<<"link">>,[],[<<"funny">>]}, + ?assertEqual(D4, iolist_to_binary(to_html(T4))), + + ok. + +parse_amp_test_() -> + [?_assertEqual( + {<<"html">>,[], + [{<<"body">>,[{<<"onload">>,<<"javascript:A('1&2')">>}],[]}]}, + mochiweb_html:parse("<html><body onload=\"javascript:A('1&2')\"></body></html>")), + ?_assertEqual( + {<<"html">>,[], + [{<<"body">>,[{<<"onload">>,<<"javascript:A('1& 2')">>}],[]}]}, + mochiweb_html:parse("<html><body onload=\"javascript:A('1& 2')\"></body></html>")), + ?_assertEqual( + {<<"html">>,[], + [{<<"body">>,[],[<<"& ">>]}]}, + mochiweb_html:parse("<html><body>& </body></html>")), + ?_assertEqual( + {<<"html">>,[], + [{<<"body">>,[],[<<"&">>]}]}, + mochiweb_html:parse("<html><body>&</body></html>"))]. + +parse_unescaped_lt_test() -> + D1 = <<"<div> < < <a href=\"/\">Back</a></div>">>, + ?assertEqual( + {<<"div">>, [], [<<" < < ">>, {<<"a">>, [{<<"href">>, <<"/">>}], + [<<"Back">>]}]}, + mochiweb_html:parse(D1)), + + D2 = <<"<div> << <a href=\"/\">Back</a></div>">>, + ?assertEqual( + {<<"div">>, [], [<<" << ">>, {<<"a">>, [{<<"href">>, <<"/">>}], + [<<"Back">>]}]}, + mochiweb_html:parse(D2)). + -endif. diff --git a/src/mochiweb/mochiweb_http.erl b/src/mochiweb/mochiweb_http.erl index 23a475270..4f7e947ac 100644 --- a/src/mochiweb/mochiweb_http.erl +++ b/src/mochiweb/mochiweb_http.erl @@ -5,13 +5,13 @@ -module(mochiweb_http). -author('bob@mochimedia.com'). --export([start/0, start/1, stop/0, stop/1]). --export([loop/2, default_body/1]). +-export([start/1, start_link/1, stop/0, stop/1]). +-export([loop/2]). -export([after_response/2, reentry/1]). -export([parse_range_request/1, range_skip_length/2]). --define(REQUEST_RECV_TIMEOUT, 300000). % timeout waiting for request line --define(HEADERS_RECV_TIMEOUT, 30000). % timeout waiting for headers +-define(REQUEST_RECV_TIMEOUT, 300000). %% timeout waiting for request line +-define(HEADERS_RECV_TIMEOUT, 30000). %% timeout waiting for headers -define(MAX_HEADERS, 1000). -define(DEFAULTS, [{name, ?MODULE}, @@ -19,9 +19,7 @@ parse_options(Options) -> {loop, HttpLoop} = proplists:lookup(loop, Options), - Loop = fun (S) -> - ?MODULE:loop(S, HttpLoop) - end, + Loop = {?MODULE, loop, [HttpLoop]}, Options1 = [{loop, Loop} | proplists:delete(loop, Options)], mochilists:set_defaults(?DEFAULTS, Options1). @@ -31,15 +29,12 @@ stop() -> stop(Name) -> mochiweb_socket_server:stop(Name). -start() -> - start([{ip, "127.0.0.1"}, - {loop, {?MODULE, default_body}}]). - %% @spec start(Options) -> ServerRet %% Options = [option()] %% Option = {name, atom()} | {ip, string() | tuple()} | {backlog, integer()} %% | {nodelay, boolean()} | {acceptor_pool_size, integer()} %% | {ssl, boolean()} | {profile_fun, undefined | (Props) -> ok} +%% | {link, false} %% @doc Start a mochiweb server. %% profile_fun is used to profile accept timing. %% After each accept, if defined, profile_fun is called with a proplist of a subset of the mochiweb_socket_server state and timing information. @@ -48,62 +43,18 @@ start() -> start(Options) -> mochiweb_socket_server:start(parse_options(Options)). -frm(Body) -> - ["<html><head></head><body>" - "<form method=\"POST\">" - "<input type=\"hidden\" value=\"message\" name=\"hidden\"/>" - "<input type=\"submit\" value=\"regular POST\">" - "</form>" - "<br />" - "<form method=\"POST\" enctype=\"multipart/form-data\"" - " action=\"/multipart\">" - "<input type=\"hidden\" value=\"multipart message\" name=\"hidden\"/>" - "<input type=\"file\" name=\"file\"/>" - "<input type=\"submit\" value=\"multipart POST\" />" - "</form>" - "<pre>", Body, "</pre>" - "</body></html>"]. - -default_body(Req, M, "/chunked") when M =:= 'GET'; M =:= 'HEAD' -> - Res = Req:ok({"text/plain", [], chunked}), - Res:write_chunk("First chunk\r\n"), - timer:sleep(5000), - Res:write_chunk("Last chunk\r\n"), - Res:write_chunk(""); -default_body(Req, M, _Path) when M =:= 'GET'; M =:= 'HEAD' -> - Body = io_lib:format("~p~n", [[{parse_qs, Req:parse_qs()}, - {parse_cookie, Req:parse_cookie()}, - Req:dump()]]), - Req:ok({"text/html", - [mochiweb_cookies:cookie("mochiweb_http", "test_cookie")], - frm(Body)}); -default_body(Req, 'POST', "/multipart") -> - Body = io_lib:format("~p~n", [[{parse_qs, Req:parse_qs()}, - {parse_cookie, Req:parse_cookie()}, - {body, Req:recv_body()}, - Req:dump()]]), - Req:ok({"text/html", [], frm(Body)}); -default_body(Req, 'POST', _Path) -> - Body = io_lib:format("~p~n", [[{parse_qs, Req:parse_qs()}, - {parse_cookie, Req:parse_cookie()}, - {parse_post, Req:parse_post()}, - Req:dump()]]), - Req:ok({"text/html", [], frm(Body)}); -default_body(Req, _Method, _Path) -> - Req:respond({501, [], []}). - -default_body(Req) -> - default_body(Req, Req:get(method), Req:get(path)). +start_link(Options) -> + mochiweb_socket_server:start_link(parse_options(Options)). loop(Socket, Body) -> - mochiweb_socket:setopts(Socket, [{packet, http}]), + ok = mochiweb_socket:setopts(Socket, [{packet, http}]), request(Socket, Body). request(Socket, Body) -> - mochiweb_socket:setopts(Socket, [{active, once}]), + ok = mochiweb_socket:setopts(Socket, [{active, once}]), receive {Protocol, _, {http_request, Method, Path, Version}} when Protocol == http orelse Protocol == ssl -> - mochiweb_socket:setopts(Socket, [{packet, httph}]), + ok = mochiweb_socket:setopts(Socket, [{packet, httph}]), headers(Socket, {Method, Path, Version}, [], Body, 0); {Protocol, _, {http_error, "\r\n"}} when Protocol == http orelse Protocol == ssl -> request(Socket, Body); @@ -112,6 +63,13 @@ request(Socket, Body) -> {tcp_closed, _} -> mochiweb_socket:close(Socket), exit(normal); + {ssl_closed, _} -> + mochiweb_socket:close(Socket), + exit(normal); + {tcp_error,_,emsgsize} -> + % R15B02 returns this then closes the socket, so close and exit + mochiweb_socket:close(Socket), + exit(normal); _Other -> handle_invalid_request(Socket) after ?REQUEST_RECV_TIMEOUT -> @@ -126,10 +84,10 @@ reentry(Body) -> headers(Socket, Request, Headers, _Body, ?MAX_HEADERS) -> %% Too many headers sent, bad request. - mochiweb_socket:setopts(Socket, [{packet, raw}]), + ok = mochiweb_socket:setopts(Socket, [{packet, raw}]), handle_invalid_request(Socket, Request, Headers); headers(Socket, Request, Headers, Body, HeaderCount) -> - mochiweb_socket:setopts(Socket, [{active, once}]), + ok = mochiweb_socket:setopts(Socket, [{active, once}]), receive {Protocol, _, http_eoh} when Protocol == http orelse Protocol == ssl -> Req = new_request(Socket, Request, Headers), @@ -141,6 +99,10 @@ headers(Socket, Request, Headers, Body, HeaderCount) -> {tcp_closed, _} -> mochiweb_socket:close(Socket), exit(normal); + {tcp_error,_,emsgsize} -> + % R15B02 returns this then closes the socket, so close and exit + mochiweb_socket:close(Socket), + exit(normal); _Other -> handle_invalid_request(Socket, Request, Headers) after ?HEADERS_RECV_TIMEOUT -> @@ -148,14 +110,19 @@ headers(Socket, Request, Headers, Body, HeaderCount) -> exit(normal) end. +call_body({M, F, A}, Req) -> + erlang:apply(M, F, [Req | A]); call_body({M, F}, Req) -> M:F(Req); call_body(Body, Req) -> Body(Req). +-spec handle_invalid_request(term()) -> no_return(). handle_invalid_request(Socket) -> - handle_invalid_request(Socket, {'GET', {abs_path, "/"}, {0,9}}, []). + handle_invalid_request(Socket, {'GET', {abs_path, "/"}, {0,9}}, []), + exit(normal). +-spec handle_invalid_request(term(), term(), term()) -> no_return(). handle_invalid_request(Socket, Request, RevHeaders) -> Req = new_request(Socket, Request, RevHeaders), Req:respond({400, [], []}), @@ -163,7 +130,7 @@ handle_invalid_request(Socket, Request, RevHeaders) -> exit(normal). new_request(Socket, Request, RevHeaders) -> - mochiweb_socket:setopts(Socket, [{packet, raw}]), + ok = mochiweb_socket:setopts(Socket, [{packet, raw}]), mochiweb:new_request({Socket, Request, lists:reverse(RevHeaders)}). after_response(Body, Req) -> @@ -174,6 +141,7 @@ after_response(Body, Req) -> exit(normal); false -> Req:cleanup(), + erlang:garbage_collect(), ?MODULE:loop(Socket, Body) end. @@ -211,6 +179,8 @@ range_skip_length(Spec, Size) -> invalid_range; {Start, End} when 0 =< Start, Start =< End, End < Size -> {Start, End - Start + 1}; + {Start, End} when 0 =< Start, Start =< End, End >= Size -> + {Start, Size - Start}; {_OutOfRange, _End} -> invalid_range end. @@ -218,8 +188,8 @@ range_skip_length(Spec, Size) -> %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). range_test() -> %% valid, single ranges @@ -265,19 +235,23 @@ range_skip_length_test() -> BodySizeLess1 = BodySize - 1, ?assertEqual({BodySizeLess1, 1}, range_skip_length({BodySize - 1, none}, BodySize)), + ?assertEqual({BodySizeLess1, 1}, + range_skip_length({BodySize - 1, BodySize+5}, BodySize)), + ?assertEqual({BodySizeLess1, 1}, + range_skip_length({BodySize - 1, BodySize}, BodySize)), %% out of range, return whole thing ?assertEqual({0, BodySize}, range_skip_length({none, BodySize + 1}, BodySize)), ?assertEqual({0, BodySize}, range_skip_length({none, -1}, BodySize)), + ?assertEqual({0, BodySize}, + range_skip_length({0, BodySize + 1}, BodySize)), %% invalid ranges ?assertEqual(invalid_range, range_skip_length({-1, 30}, BodySize)), ?assertEqual(invalid_range, - range_skip_length({0, BodySize + 1}, BodySize)), - ?assertEqual(invalid_range, range_skip_length({-1, BodySize + 1}, BodySize)), ?assertEqual(invalid_range, range_skip_length({BodySize, 40}, BodySize)), diff --git a/src/mochiweb/mochiweb_io.erl b/src/mochiweb/mochiweb_io.erl index 6ce57ec8f..8454b43da 100644 --- a/src/mochiweb/mochiweb_io.erl +++ b/src/mochiweb/mochiweb_io.erl @@ -38,9 +38,6 @@ iodevice_size(IoDevice) -> %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). - - - +-include_lib("eunit/include/eunit.hrl"). -endif. diff --git a/src/mochiweb/mochiweb_mime.erl b/src/mochiweb/mochiweb_mime.erl index 5344aee7a..7d9f2493c 100644 --- a/src/mochiweb/mochiweb_mime.erl +++ b/src/mochiweb/mochiweb_mime.erl @@ -11,72 +11,393 @@ %% @doc Given a filename extension (e.g. ".html") return a guess for the MIME %% type such as "text/html". Will return the atom undefined if no good %% guess is available. -from_extension(".html") -> - "text/html"; -from_extension(".xhtml") -> - "application/xhtml+xml"; -from_extension(".xml") -> - "application/xml"; -from_extension(".css") -> - "text/css"; + +from_extension(".stl") -> + "application/SLA"; +from_extension(".stp") -> + "application/STEP"; +from_extension(".step") -> + "application/STEP"; +from_extension(".dwg") -> + "application/acad"; +from_extension(".ez") -> + "application/andrew-inset"; +from_extension(".ccad") -> + "application/clariscad"; +from_extension(".drw") -> + "application/drafting"; +from_extension(".tsp") -> + "application/dsptype"; +from_extension(".dxf") -> + "application/dxf"; +from_extension(".xls") -> + "application/excel"; +from_extension(".unv") -> + "application/i-deas"; +from_extension(".jar") -> + "application/java-archive"; +from_extension(".hqx") -> + "application/mac-binhex40"; +from_extension(".cpt") -> + "application/mac-compactpro"; +from_extension(".pot") -> + "application/vnd.ms-powerpoint"; +from_extension(".ppt") -> + "application/vnd.ms-powerpoint"; +from_extension(".dms") -> + "application/octet-stream"; +from_extension(".lha") -> + "application/octet-stream"; +from_extension(".lzh") -> + "application/octet-stream"; +from_extension(".oda") -> + "application/oda"; +from_extension(".ogg") -> + "application/ogg"; +from_extension(".ogm") -> + "application/ogg"; +from_extension(".pdf") -> + "application/pdf"; +from_extension(".pgp") -> + "application/pgp"; +from_extension(".ai") -> + "application/postscript"; +from_extension(".eps") -> + "application/postscript"; +from_extension(".ps") -> + "application/postscript"; +from_extension(".prt") -> + "application/pro_eng"; +from_extension(".rtf") -> + "application/rtf"; +from_extension(".smi") -> + "application/smil"; +from_extension(".smil") -> + "application/smil"; +from_extension(".sol") -> + "application/solids"; +from_extension(".vda") -> + "application/vda"; +from_extension(".xlm") -> + "application/vnd.ms-excel"; +from_extension(".cod") -> + "application/vnd.rim.cod"; +from_extension(".pgn") -> + "application/x-chess-pgn"; +from_extension(".cpio") -> + "application/x-cpio"; +from_extension(".csh") -> + "application/x-csh"; +from_extension(".deb") -> + "application/x-debian-package"; +from_extension(".dcr") -> + "application/x-director"; +from_extension(".dir") -> + "application/x-director"; +from_extension(".dxr") -> + "application/x-director"; +from_extension(".gz") -> + "application/x-gzip"; +from_extension(".hdf") -> + "application/x-hdf"; +from_extension(".ipx") -> + "application/x-ipix"; +from_extension(".ips") -> + "application/x-ipscript"; from_extension(".js") -> "application/x-javascript"; -from_extension(".jpg") -> - "image/jpeg"; -from_extension(".gif") -> - "image/gif"; -from_extension(".png") -> - "image/png"; +from_extension(".skd") -> + "application/x-koan"; +from_extension(".skm") -> + "application/x-koan"; +from_extension(".skp") -> + "application/x-koan"; +from_extension(".skt") -> + "application/x-koan"; +from_extension(".latex") -> + "application/x-latex"; +from_extension(".lsp") -> + "application/x-lisp"; +from_extension(".scm") -> + "application/x-lotusscreencam"; +from_extension(".mif") -> + "application/x-mif"; +from_extension(".com") -> + "application/x-msdos-program"; +from_extension(".exe") -> + "application/octet-stream"; +from_extension(".cdf") -> + "application/x-netcdf"; +from_extension(".nc") -> + "application/x-netcdf"; +from_extension(".pl") -> + "application/x-perl"; +from_extension(".pm") -> + "application/x-perl"; +from_extension(".rar") -> + "application/x-rar-compressed"; +from_extension(".sh") -> + "application/x-sh"; +from_extension(".shar") -> + "application/x-shar"; from_extension(".swf") -> "application/x-shockwave-flash"; -from_extension(".zip") -> - "application/zip"; -from_extension(".bz2") -> - "application/x-bzip2"; -from_extension(".gz") -> - "application/x-gzip"; +from_extension(".sit") -> + "application/x-stuffit"; +from_extension(".sv4cpio") -> + "application/x-sv4cpio"; +from_extension(".sv4crc") -> + "application/x-sv4crc"; +from_extension(".tar.gz") -> + "application/x-tar-gz"; +from_extension(".tgz") -> + "application/x-tar-gz"; from_extension(".tar") -> "application/x-tar"; -from_extension(".tgz") -> - "application/x-gzip"; +from_extension(".tcl") -> + "application/x-tcl"; +from_extension(".texi") -> + "application/x-texinfo"; +from_extension(".texinfo") -> + "application/x-texinfo"; +from_extension(".man") -> + "application/x-troff-man"; +from_extension(".me") -> + "application/x-troff-me"; +from_extension(".ms") -> + "application/x-troff-ms"; +from_extension(".roff") -> + "application/x-troff"; +from_extension(".t") -> + "application/x-troff"; +from_extension(".tr") -> + "application/x-troff"; +from_extension(".ustar") -> + "application/x-ustar"; +from_extension(".src") -> + "application/x-wais-source"; +from_extension(".zip") -> + "application/zip"; +from_extension(".tsi") -> + "audio/TSP-audio"; +from_extension(".au") -> + "audio/basic"; +from_extension(".snd") -> + "audio/basic"; +from_extension(".kar") -> + "audio/midi"; +from_extension(".mid") -> + "audio/midi"; +from_extension(".midi") -> + "audio/midi"; +from_extension(".mp2") -> + "audio/mpeg"; +from_extension(".mp3") -> + "audio/mpeg"; +from_extension(".mpga") -> + "audio/mpeg"; +from_extension(".aif") -> + "audio/x-aiff"; +from_extension(".aifc") -> + "audio/x-aiff"; +from_extension(".aiff") -> + "audio/x-aiff"; +from_extension(".m3u") -> + "audio/x-mpegurl"; +from_extension(".wax") -> + "audio/x-ms-wax"; +from_extension(".wma") -> + "audio/x-ms-wma"; +from_extension(".rpm") -> + "audio/x-pn-realaudio-plugin"; +from_extension(".ram") -> + "audio/x-pn-realaudio"; +from_extension(".rm") -> + "audio/x-pn-realaudio"; +from_extension(".ra") -> + "audio/x-realaudio"; +from_extension(".wav") -> + "audio/x-wav"; +from_extension(".pdb") -> + "chemical/x-pdb"; +from_extension(".ras") -> + "image/cmu-raster"; +from_extension(".gif") -> + "image/gif"; +from_extension(".ief") -> + "image/ief"; +from_extension(".jpe") -> + "image/jpeg"; +from_extension(".jpeg") -> + "image/jpeg"; +from_extension(".jpg") -> + "image/jpeg"; +from_extension(".jp2") -> + "image/jp2"; +from_extension(".png") -> + "image/png"; +from_extension(".tif") -> + "image/tiff"; +from_extension(".tiff") -> + "image/tiff"; +from_extension(".pnm") -> + "image/x-portable-anymap"; +from_extension(".pbm") -> + "image/x-portable-bitmap"; +from_extension(".pgm") -> + "image/x-portable-graymap"; +from_extension(".ppm") -> + "image/x-portable-pixmap"; +from_extension(".rgb") -> + "image/x-rgb"; +from_extension(".xbm") -> + "image/x-xbitmap"; +from_extension(".xwd") -> + "image/x-xwindowdump"; +from_extension(".iges") -> + "model/iges"; +from_extension(".igs") -> + "model/iges"; +from_extension(".mesh") -> + "model/mesh"; +from_extension(".") -> + ""; +from_extension(".msh") -> + "model/mesh"; +from_extension(".silo") -> + "model/mesh"; +from_extension(".vrml") -> + "model/vrml"; +from_extension(".wrl") -> + "model/vrml"; +from_extension(".css") -> + "text/css"; +from_extension(".htm") -> + "text/html"; +from_extension(".html") -> + "text/html"; +from_extension(".asc") -> + "text/plain"; +from_extension(".c") -> + "text/plain"; +from_extension(".cc") -> + "text/plain"; +from_extension(".f90") -> + "text/plain"; +from_extension(".f") -> + "text/plain"; +from_extension(".hh") -> + "text/plain"; +from_extension(".m") -> + "text/plain"; from_extension(".txt") -> "text/plain"; -from_extension(".doc") -> - "application/msword"; -from_extension(".pdf") -> - "application/pdf"; -from_extension(".xls") -> - "application/vnd.ms-excel"; -from_extension(".rtf") -> - "application/rtf"; +from_extension(".rtx") -> + "text/richtext"; +from_extension(".sgm") -> + "text/sgml"; +from_extension(".sgml") -> + "text/sgml"; +from_extension(".tsv") -> + "text/tab-separated-values"; +from_extension(".jad") -> + "text/vnd.sun.j2me.app-descriptor"; +from_extension(".etx") -> + "text/x-setext"; +from_extension(".xml") -> + "application/xml"; +from_extension(".dl") -> + "video/dl"; +from_extension(".fli") -> + "video/fli"; +from_extension(".flv") -> + "video/x-flv"; +from_extension(".gl") -> + "video/gl"; +from_extension(".mp4") -> + "video/mp4"; +from_extension(".mpe") -> + "video/mpeg"; +from_extension(".mpeg") -> + "video/mpeg"; +from_extension(".mpg") -> + "video/mpeg"; from_extension(".mov") -> "video/quicktime"; -from_extension(".mp3") -> - "audio/mpeg"; +from_extension(".qt") -> + "video/quicktime"; +from_extension(".viv") -> + "video/vnd.vivo"; +from_extension(".vivo") -> + "video/vnd.vivo"; +from_extension(".asf") -> + "video/x-ms-asf"; +from_extension(".asx") -> + "video/x-ms-asx"; +from_extension(".wmv") -> + "video/x-ms-wmv"; +from_extension(".wmx") -> + "video/x-ms-wmx"; +from_extension(".wvx") -> + "video/x-ms-wvx"; +from_extension(".avi") -> + "video/x-msvideo"; +from_extension(".movie") -> + "video/x-sgi-movie"; +from_extension(".mime") -> + "www/mime"; +from_extension(".ice") -> + "x-conference/x-cooltalk"; +from_extension(".vrm") -> + "x-world/x-vrml"; +from_extension(".spx") -> + "audio/ogg"; +from_extension(".xhtml") -> + "application/xhtml+xml"; +from_extension(".bz2") -> + "application/x-bzip2"; +from_extension(".doc") -> + "application/msword"; from_extension(".z") -> "application/x-compress"; -from_extension(".wav") -> - "audio/x-wav"; from_extension(".ico") -> "image/x-icon"; from_extension(".bmp") -> "image/bmp"; from_extension(".m4a") -> "audio/mpeg"; -from_extension(".m3u") -> - "audio/x-mpegurl"; -from_extension(".exe") -> - "application/octet-stream"; from_extension(".csv") -> "text/csv"; +from_extension(".eot") -> + "application/vnd.ms-fontobject"; +from_extension(".m4v") -> + "video/mp4"; +from_extension(".svg") -> + "image/svg+xml"; +from_extension(".svgz") -> + "image/svg+xml"; +from_extension(".ttc") -> + "application/x-font-ttf"; +from_extension(".ttf") -> + "application/x-font-ttf"; +from_extension(".vcf") -> + "text/x-vcard"; +from_extension(".webm") -> + "video/web"; +from_extension(".webp") -> + "image/web"; +from_extension(".woff") -> + "application/x-font-woff"; +from_extension(".otf") -> + "font/opentype"; from_extension(_) -> undefined. %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). exhaustive_from_extension_test() -> T = mochiweb_cover:clause_lookup_table(?MODULE, from_extension), diff --git a/src/mochiweb/mochiweb_multipart.erl b/src/mochiweb/mochiweb_multipart.erl index 3069cf4d5..a83a88c98 100644 --- a/src/mochiweb/mochiweb_multipart.erl +++ b/src/mochiweb/mochiweb_multipart.erl @@ -128,7 +128,7 @@ default_file_handler_1(Filename, ContentType, Acc) -> parse_multipart_request(Req, Callback) -> %% TODO: Support chunked? - Length = list_to_integer(Req:get_header_value("content-length")), + Length = list_to_integer(Req:get_combined_header_value("content-length")), Boundary = iolist_to_binary( get_boundary(Req:get_header_value("content-type"))), Prefix = <<"\r\n--", Boundary/binary>>, @@ -240,24 +240,22 @@ get_boundary(ContentType) -> S end. -find_in_binary(B, Data) when size(B) > 0 -> - case size(Data) - size(B) of +%% @spec find_in_binary(Pattern::binary(), Data::binary()) -> +%% {exact, N} | {partial, N, K} | not_found +%% @doc Searches for the given pattern in the given binary. +find_in_binary(P, Data) when size(P) > 0 -> + PS = size(P), + DS = size(Data), + case DS - PS of Last when Last < 0 -> - partial_find(B, Data, 0, size(Data)); + partial_find(P, Data, 0, DS); Last -> - find_in_binary(B, size(B), Data, 0, Last) + case binary:match(Data, P) of + {Pos, _} -> {exact, Pos}; + nomatch -> partial_find(P, Data, Last+1, PS-1) + end end. -find_in_binary(B, BS, D, N, Last) when N =< Last-> - case D of - <<_:N/binary, B:BS/binary, _/binary>> -> - {exact, N}; - _ -> - find_in_binary(B, BS, D, 1 + N, Last) - end; -find_in_binary(B, BS, D, N, Last) when N =:= 1 + Last -> - partial_find(B, D, N, BS - 1). - partial_find(_B, _D, _N, 0) -> not_found; partial_find(B, D, N, K) -> @@ -295,8 +293,8 @@ find_boundary(Prefix, Data) -> %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). ssl_cert_opts() -> EbinDir = filename:dirname(code:which(?MODULE)), @@ -313,7 +311,7 @@ with_socket_server(Transport, ServerFun, ClientFun) -> ssl -> ServerOpts0 ++ [{ssl, true}, {ssl_opts, ssl_cert_opts()}] end, - {ok, Server} = mochiweb_socket_server:start(ServerOpts), + {ok, Server} = mochiweb_socket_server:start_link(ServerOpts), Port = mochiweb_socket_server:get(Server, port), ClientOpts = [binary, {active, false}], {ok, Client} = case Transport of @@ -378,7 +376,7 @@ parse3(Transport) -> TestCallback = fun (Next) -> test_callback(Next, Expect) end, ServerFun = fun (Socket) -> ok = mochiweb_socket:send(Socket, BinContent), - exit(normal) + exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, @@ -414,7 +412,7 @@ parse2(Transport) -> TestCallback = fun (Next) -> test_callback(Next, Expect) end, ServerFun = fun (Socket) -> ok = mochiweb_socket:send(Socket, BinContent), - exit(normal) + exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, @@ -451,7 +449,7 @@ do_parse_form(Transport) -> BinContent = iolist_to_binary(Content), ServerFun = fun (Socket) -> ok = mochiweb_socket:send(Socket, BinContent), - exit(normal) + exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, @@ -504,7 +502,7 @@ do_parse(Transport) -> TestCallback = fun (Next) -> test_callback(Next, Expect) end, ServerFun = fun (Socket) -> ok = mochiweb_socket:send(Socket, BinContent), - exit(normal) + exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, @@ -556,7 +554,7 @@ parse_partial_body_boundary(Transport) -> TestCallback = fun (Next) -> test_callback(Next, Expect) end, ServerFun = fun (Socket) -> ok = mochiweb_socket:send(Socket, BinContent), - exit(normal) + exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, @@ -609,7 +607,7 @@ parse_large_header(Transport) -> TestCallback = fun (Next) -> test_callback(Next, Expect) end, ServerFun = fun (Socket) -> ok = mochiweb_socket:send(Socket, BinContent), - exit(normal) + exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, @@ -685,7 +683,7 @@ flash_parse(Transport) -> TestCallback = fun (Next) -> test_callback(Next, Expect) end, ServerFun = fun (Socket) -> ok = mochiweb_socket:send(Socket, BinContent), - exit(normal) + exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, @@ -733,7 +731,7 @@ flash_parse2(Transport) -> TestCallback = fun (Next) -> test_callback(Next, Expect) end, ServerFun = fun (Socket) -> ok = mochiweb_socket:send(Socket, BinContent), - exit(normal) + exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, @@ -821,4 +819,54 @@ multipart_body_test() -> 10))), ok. +%% @todo Move somewhere more appropriate than in the test suite + +multipart_parsing_benchmark_test() -> + run_multipart_parsing_benchmark(1). + +run_multipart_parsing_benchmark(0) -> ok; +run_multipart_parsing_benchmark(N) -> + multipart_parsing_benchmark(), + run_multipart_parsing_benchmark(N-1). + +multipart_parsing_benchmark() -> + ContentType = "multipart/form-data; boundary=----------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5", + Chunk = binary:copy(<<"This Is_%Some=Quite0Long4String2Used9For7BenchmarKing.5">>, 102400), + BinContent = <<"------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5\r\nContent-Disposition: form-data; name=\"Filename\"\r\n\r\nhello.txt\r\n------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5\r\nContent-Disposition: form-data; name=\"success_action_status\"\r\n\r\n201\r\n------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5\r\nContent-Disposition: form-data; name=\"file\"; filename=\"hello.txt\"\r\nContent-Type: application/octet-stream\r\n\r\n", Chunk/binary, "\r\n------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5\r\nContent-Disposition: form-data; name=\"Upload\"\r\n\r\nSubmit Query\r\n------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5--">>, + Expect = [{headers, + [{"content-disposition", + {"form-data", [{"name", "Filename"}]}}]}, + {body, <<"hello.txt">>}, + body_end, + {headers, + [{"content-disposition", + {"form-data", [{"name", "success_action_status"}]}}]}, + {body, <<"201">>}, + body_end, + {headers, + [{"content-disposition", + {"form-data", [{"name", "file"}, {"filename", "hello.txt"}]}}, + {"content-type", {"application/octet-stream", []}}]}, + {body, Chunk}, + body_end, + {headers, + [{"content-disposition", + {"form-data", [{"name", "Upload"}]}}]}, + {body, <<"Submit Query">>}, + body_end, + eof], + TestCallback = fun (Next) -> test_callback(Next, Expect) end, + ServerFun = fun (Socket) -> + ok = mochiweb_socket:send(Socket, BinContent), + exit(normal) + end, + ClientFun = fun (Socket) -> + Req = fake_request(Socket, ContentType, + byte_size(BinContent)), + Res = parse_multipart_request(Req, TestCallback), + {0, <<>>, ok} = Res, + ok + end, + ok = with_socket_server(plain, ServerFun, ClientFun), + ok. -endif. diff --git a/src/mochiweb/mochiweb_request.erl b/src/mochiweb/mochiweb_request.erl index 980f5ad01..1b431d37a 100644 --- a/src/mochiweb/mochiweb_request.erl +++ b/src/mochiweb/mochiweb_request.erl @@ -3,7 +3,7 @@ %% @doc MochiWeb HTTP Request abstraction. --module(mochiweb_request, [Socket, Method, RawPath, Version, Headers]). +-module(mochiweb_request). -author('bob@mochimedia.com'). -include_lib("kernel/include/file.hrl"). @@ -11,17 +11,18 @@ -define(QUIP, "Any of you quaids got a smint?"). --export([get_header_value/1, get_primary_header_value/1, get/1, dump/0]). --export([send/1, recv/1, recv/2, recv_body/0, recv_body/1, stream_body/3]). --export([start_response/1, start_response_length/1, start_raw_response/1]). --export([respond/1, ok/1]). --export([not_found/0, not_found/1]). --export([parse_post/0, parse_qs/0]). --export([should_close/0, cleanup/0]). --export([parse_cookie/0, get_cookie_value/1]). --export([serve_file/2, serve_file/3]). --export([accepted_encodings/1]). --export([accepts_content_type/1]). +-export([new/5]). +-export([get_header_value/2, get_primary_header_value/2, get_combined_header_value/2, get/2, dump/1]). +-export([send/2, recv/2, recv/3, recv_body/1, recv_body/2, stream_body/4]). +-export([start_response/2, start_response_length/2, start_raw_response/2]). +-export([respond/2, ok/2]). +-export([not_found/1, not_found/2]). +-export([parse_post/1, parse_qs/1]). +-export([should_close/1, cleanup/1]). +-export([parse_cookie/1, get_cookie_value/2]). +-export([serve_file/3, serve_file/4]). +-export([accepted_encodings/2]). +-export([accepts_content_type/2, accepted_content_types/2]). -define(SAVE_QS, mochiweb_request_qs). -define(SAVE_PATH, mochiweb_request_path). @@ -32,11 +33,10 @@ -define(SAVE_COOKIE, mochiweb_request_cookie). -define(SAVE_FORCE_CLOSE, mochiweb_request_force_close). -%% @type iolist() = [iolist() | binary() | char()]. -%% @type iodata() = binary() | iolist(). %% @type key() = atom() | string() | binary() %% @type value() = atom() | string() | binary() | integer() %% @type headers(). A mochiweb_headers structure. +%% @type request() = {mochiweb_request,[_Socket,_Method,_RawPath,_Version,_Headers]} %% @type response(). A mochiweb_response parameterized module instance. %% @type ioheaders() = headers() | [{key(), value()}]. @@ -46,50 +46,58 @@ % Maximum recv_body() length of 1MB -define(MAX_RECV_BODY, (1024*1024)). -%% @spec get_header_value(K) -> undefined | Value +%% @spec new(Socket, Method, RawPath, Version, headers()) -> request() +%% @doc Create a new request instance. +new(Socket, Method, RawPath, Version, Headers) -> + {?MODULE, [Socket, Method, RawPath, Version, Headers]}. + +%% @spec get_header_value(K, request()) -> undefined | Value %% @doc Get the value of a given request header. -get_header_value(K) -> +get_header_value(K, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) -> mochiweb_headers:get_value(K, Headers). -get_primary_header_value(K) -> +get_primary_header_value(K, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) -> mochiweb_headers:get_primary_value(K, Headers). +get_combined_header_value(K, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) -> + mochiweb_headers:get_combined_value(K, Headers). + %% @type field() = socket | scheme | method | raw_path | version | headers | peer | path | body_length | range -%% @spec get(field()) -> term() +%% @spec get(field(), request()) -> term() %% @doc Return the internal representation of the given field. If %% <code>socket</code> is requested on a HTTPS connection, then %% an ssl socket will be returned as <code>{ssl, SslSocket}</code>. %% You can use <code>SslSocket</code> with the <code>ssl</code> %% application, eg: <code>ssl:peercert(SslSocket)</code>. -get(socket) -> +get(socket, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) -> Socket; -get(scheme) -> +get(scheme, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) -> case mochiweb_socket:type(Socket) of plain -> http; ssl -> https end; -get(method) -> +get(method, {?MODULE, [_Socket, Method, _RawPath, _Version, _Headers]}) -> Method; -get(raw_path) -> +get(raw_path, {?MODULE, [_Socket, _Method, RawPath, _Version, _Headers]}) -> RawPath; -get(version) -> +get(version, {?MODULE, [_Socket, _Method, _RawPath, Version, _Headers]}) -> Version; -get(headers) -> +get(headers, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) -> Headers; -get(peer) -> +get(peer, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> case mochiweb_socket:peername(Socket) of {ok, {Addr={10, _, _, _}, _Port}} -> - case get_header_value("x-forwarded-for") of + case get_header_value("x-forwarded-for", THIS) of undefined -> inet_parse:ntoa(Addr); Hosts -> string:strip(lists:last(string:tokens(Hosts, ","))) end; {ok, {{127, 0, 0, 1}, _Port}} -> - case get_header_value("x-forwarded-for") of + case get_header_value("x-forwarded-for", THIS) of undefined -> "127.0.0.1"; Hosts -> @@ -100,7 +108,7 @@ get(peer) -> {error, enotconn} -> exit(normal) end; -get(path) -> +get(path, {?MODULE, [_Socket, _Method, RawPath, _Version, _Headers]}) -> case erlang:get(?SAVE_PATH) of undefined -> {Path0, _, _} = mochiweb_util:urlsplit_path(RawPath), @@ -110,35 +118,35 @@ get(path) -> Cached -> Cached end; -get(body_length) -> +get(body_length, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> case erlang:get(?SAVE_BODY_LENGTH) of undefined -> - BodyLength = body_length(), + BodyLength = body_length(THIS), put(?SAVE_BODY_LENGTH, {cached, BodyLength}), BodyLength; {cached, Cached} -> Cached end; -get(range) -> - case get_header_value(range) of +get(range, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + case get_header_value(range, THIS) of undefined -> undefined; RawRange -> mochiweb_http:parse_range_request(RawRange) end. -%% @spec dump() -> {mochiweb_request, [{atom(), term()}]} +%% @spec dump(request()) -> {mochiweb_request, [{atom(), term()}]} %% @doc Dump the internal representation to a "human readable" set of terms %% for debugging/inspection purposes. -dump() -> +dump({?MODULE, [_Socket, Method, RawPath, Version, Headers]}) -> {?MODULE, [{method, Method}, {version, Version}, {raw_path, RawPath}, {headers, mochiweb_headers:to_list(Headers)}]}. -%% @spec send(iodata()) -> ok +%% @spec send(iodata(), request()) -> ok %% @doc Send data over the socket. -send(Data) -> +send(Data, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) -> case mochiweb_socket:send(Socket, Data) of ok -> ok; @@ -146,16 +154,16 @@ send(Data) -> exit(normal) end. -%% @spec recv(integer()) -> binary() +%% @spec recv(integer(), request()) -> binary() %% @doc Receive Length bytes from the client as a binary, with the default %% idle timeout. -recv(Length) -> - recv(Length, ?IDLE_TIMEOUT). +recv(Length, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + recv(Length, ?IDLE_TIMEOUT, THIS). -%% @spec recv(integer(), integer()) -> binary() +%% @spec recv(integer(), integer(), request()) -> binary() %% @doc Receive Length bytes from the client as a binary, with the given %% Timeout in msec. -recv(Length, Timeout) -> +recv(Length, Timeout, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) -> case mochiweb_socket:recv(Socket, Length, Timeout) of {ok, Data} -> put(?SAVE_RECV, true), @@ -164,12 +172,12 @@ recv(Length, Timeout) -> exit(normal) end. -%% @spec body_length() -> undefined | chunked | unknown_transfer_encoding | integer() +%% @spec body_length(request()) -> undefined | chunked | unknown_transfer_encoding | integer() %% @doc Infer body length from transfer-encoding and content-length headers. -body_length() -> - case get_header_value("transfer-encoding") of +body_length({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + case get_header_value("transfer-encoding", THIS) of undefined -> - case get_header_value("content-length") of + case get_combined_header_value("content-length", THIS) of undefined -> undefined; Length -> @@ -182,16 +190,16 @@ body_length() -> end. -%% @spec recv_body() -> binary() +%% @spec recv_body(request()) -> binary() %% @doc Receive the body of the HTTP request (defined by Content-Length). %% Will only receive up to the default max-body length of 1MB. -recv_body() -> - recv_body(?MAX_RECV_BODY). +recv_body({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + recv_body(?MAX_RECV_BODY, THIS). -%% @spec recv_body(integer()) -> binary() +%% @spec recv_body(integer(), request()) -> binary() %% @doc Receive the body of the HTTP request (defined by Content-Length). %% Will receive up to MaxBody bytes. -recv_body(MaxBody) -> +recv_body(MaxBody, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> case erlang:get(?SAVE_BODY) of undefined -> % we could use a sane constant for max chunk size @@ -205,17 +213,18 @@ recv_body(MaxBody) -> true -> {NewLength, [Bin | BinAcc]} end - end, {0, []}, MaxBody), + end, {0, []}, MaxBody, THIS), put(?SAVE_BODY, Body), Body; Cached -> Cached end. -stream_body(MaxChunkSize, ChunkFun, FunState) -> - stream_body(MaxChunkSize, ChunkFun, FunState, undefined). +stream_body(MaxChunkSize, ChunkFun, FunState, {?MODULE,[_Socket,_Method,_RawPath,_Version,_Headers]}=THIS) -> + stream_body(MaxChunkSize, ChunkFun, FunState, undefined, THIS). -stream_body(MaxChunkSize, ChunkFun, FunState, MaxBodyLength) -> - Expect = case get_header_value("expect") of +stream_body(MaxChunkSize, ChunkFun, FunState, MaxBodyLength, + {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + Expect = case get_header_value("expect", THIS) of undefined -> undefined; Value when is_list(Value) -> @@ -223,11 +232,12 @@ stream_body(MaxChunkSize, ChunkFun, FunState, MaxBodyLength) -> end, case Expect of "100-continue" -> - start_raw_response({100, gb_trees:empty()}); + _ = start_raw_response({100, gb_trees:empty()}, THIS), + ok; _Else -> ok end, - case body_length() of + case body_length(THIS) of undefined -> undefined; {unknown_transfer_encoding, Unknown} -> @@ -236,7 +246,7 @@ stream_body(MaxChunkSize, ChunkFun, FunState, MaxBodyLength) -> % In this case the MaxBody is actually used to % determine the maximum allowed size of a single % chunk. - stream_chunked_body(MaxChunkSize, ChunkFun, FunState); + stream_chunked_body(MaxChunkSize, ChunkFun, FunState, THIS); 0 -> <<>>; Length when is_integer(Length) -> @@ -244,62 +254,64 @@ stream_body(MaxChunkSize, ChunkFun, FunState, MaxBodyLength) -> MaxBodyLength when is_integer(MaxBodyLength), MaxBodyLength < Length -> exit({body_too_large, content_length}); _ -> - stream_unchunked_body(Length, ChunkFun, FunState) - end; - Length -> - exit({length_not_integer, Length}) + stream_unchunked_body(Length, ChunkFun, FunState, THIS) + end end. -%% @spec start_response({integer(), ioheaders()}) -> response() +%% @spec start_response({integer(), ioheaders()}, request()) -> response() %% @doc Start the HTTP response by sending the Code HTTP response and %% ResponseHeaders. The server will set header defaults such as Server %% and Date if not present in ResponseHeaders. -start_response({Code, ResponseHeaders}) -> +start_response({Code, ResponseHeaders}, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> HResponse = mochiweb_headers:make(ResponseHeaders), HResponse1 = mochiweb_headers:default_from_list(server_headers(), HResponse), - start_raw_response({Code, HResponse1}). + start_raw_response({Code, HResponse1}, THIS). -%% @spec start_raw_response({integer(), headers()}) -> response() +%% @spec start_raw_response({integer(), headers()}, request()) -> response() %% @doc Start the HTTP response by sending the Code HTTP response and %% ResponseHeaders. -start_raw_response({Code, ResponseHeaders}) -> +start_raw_response({Code, ResponseHeaders}, {?MODULE, [_Socket, _Method, _RawPath, Version, _Headers]}=THIS) -> F = fun ({K, V}, Acc) -> [mochiweb_util:make_io(K), <<": ">>, V, <<"\r\n">> | Acc] end, End = lists:foldl(F, [<<"\r\n">>], mochiweb_headers:to_list(ResponseHeaders)), - send([make_version(Version), make_code(Code), <<"\r\n">> | End]), + send([make_version(Version), make_code(Code), <<"\r\n">> | End], THIS), mochiweb:new_response({THIS, Code, ResponseHeaders}). -%% @spec start_response_length({integer(), ioheaders(), integer()}) -> response() +%% @spec start_response_length({integer(), ioheaders(), integer()}, request()) -> response() %% @doc Start the HTTP response by sending the Code HTTP response and %% ResponseHeaders including a Content-Length of Length. The server %% will set header defaults such as Server %% and Date if not present in ResponseHeaders. -start_response_length({Code, ResponseHeaders, Length}) -> +start_response_length({Code, ResponseHeaders, Length}, + {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> HResponse = mochiweb_headers:make(ResponseHeaders), HResponse1 = mochiweb_headers:enter("Content-Length", Length, HResponse), - start_response({Code, HResponse1}). + start_response({Code, HResponse1}, THIS). -%% @spec respond({integer(), ioheaders(), iodata() | chunked | {file, IoDevice}}) -> response() +%% @spec respond({integer(), ioheaders(), iodata() | chunked | {file, IoDevice}}, request()) -> response() %% @doc Start the HTTP response with start_response, and send Body to the %% client (if the get(method) /= 'HEAD'). The Content-Length header %% will be set by the Body length, and the server will insert header %% defaults. -respond({Code, ResponseHeaders, {file, IoDevice}}) -> +respond({Code, ResponseHeaders, {file, IoDevice}}, + {?MODULE, [_Socket, Method, _RawPath, _Version, _Headers]}=THIS) -> Length = mochiweb_io:iodevice_size(IoDevice), - Response = start_response_length({Code, ResponseHeaders, Length}), + Response = start_response_length({Code, ResponseHeaders, Length}, THIS), case Method of 'HEAD' -> ok; _ -> - mochiweb_io:iodevice_stream(fun send/1, IoDevice) + mochiweb_io:iodevice_stream( + fun (Body) -> send(Body, THIS) end, + IoDevice) end, Response; -respond({Code, ResponseHeaders, chunked}) -> +respond({Code, ResponseHeaders, chunked}, {?MODULE, [_Socket, Method, _RawPath, Version, _Headers]}=THIS) -> HResponse = mochiweb_headers:make(ResponseHeaders), HResponse1 = case Method of 'HEAD' -> @@ -320,35 +332,35 @@ respond({Code, ResponseHeaders, chunked}) -> put(?SAVE_FORCE_CLOSE, true), HResponse end, - start_response({Code, HResponse1}); -respond({Code, ResponseHeaders, Body}) -> - Response = start_response_length({Code, ResponseHeaders, iolist_size(Body)}), + start_response({Code, HResponse1}, THIS); +respond({Code, ResponseHeaders, Body}, {?MODULE, [_Socket, Method, _RawPath, _Version, _Headers]}=THIS) -> + Response = start_response_length({Code, ResponseHeaders, iolist_size(Body)}, THIS), case Method of 'HEAD' -> ok; _ -> - send(Body) + send(Body, THIS) end, Response. -%% @spec not_found() -> response() +%% @spec not_found(request()) -> response() %% @doc Alias for <code>not_found([])</code>. -not_found() -> - not_found([]). +not_found({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + not_found([], THIS). -%% @spec not_found(ExtraHeaders) -> response() +%% @spec not_found(ExtraHeaders, request()) -> response() %% @doc Alias for <code>respond({404, [{"Content-Type", "text/plain"} %% | ExtraHeaders], <<"Not found.">>})</code>. -not_found(ExtraHeaders) -> +not_found(ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> respond({404, [{"Content-Type", "text/plain"} | ExtraHeaders], - <<"Not found.">>}). + <<"Not found.">>}, THIS). -%% @spec ok({value(), iodata()} | {value(), ioheaders(), iodata() | {file, IoDevice}}) -> +%% @spec ok({value(), iodata()} | {value(), ioheaders(), iodata() | {file, IoDevice}}, request()) -> %% response() %% @doc respond({200, [{"Content-Type", ContentType} | Headers], Body}). -ok({ContentType, Body}) -> - ok({ContentType, [], Body}); -ok({ContentType, ResponseHeaders, Body}) -> +ok({ContentType, Body}, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + ok({ContentType, [], Body}, THIS); +ok({ContentType, ResponseHeaders, Body}, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> HResponse = mochiweb_headers:make(ResponseHeaders), case THIS:get(range) of X when (X =:= undefined orelse X =:= fail) orelse Body =:= chunked -> @@ -357,7 +369,7 @@ ok({ContentType, ResponseHeaders, Body}) -> %% full response. HResponse1 = mochiweb_headers:enter("Content-Type", ContentType, HResponse), - respond({200, HResponse1, Body}); + respond({200, HResponse1, Body}, THIS); Ranges -> {PartList, Size} = range_parts(Body, Ranges), case PartList of @@ -366,7 +378,7 @@ ok({ContentType, ResponseHeaders, Body}) -> ContentType, HResponse), %% could be 416, for now we'll just return 200 - respond({200, HResponse1, Body}); + respond({200, HResponse1, Body}, THIS); PartList -> {RangeHeaders, RangeBody} = mochiweb_multipart:parts_to_body(PartList, ContentType, Size), @@ -374,46 +386,50 @@ ok({ContentType, ResponseHeaders, Body}) -> [{"Accept-Ranges", "bytes"} | RangeHeaders], HResponse), - respond({206, HResponse1, RangeBody}) + respond({206, HResponse1, RangeBody}, THIS) end end. -%% @spec should_close() -> bool() +%% @spec should_close(request()) -> bool() %% @doc Return true if the connection must be closed. If false, using %% Keep-Alive should be safe. -should_close() -> +should_close({?MODULE, [_Socket, _Method, _RawPath, Version, _Headers]}=THIS) -> ForceClose = erlang:get(?SAVE_FORCE_CLOSE) =/= undefined, DidNotRecv = erlang:get(?SAVE_RECV) =:= undefined, ForceClose orelse Version < {1, 0} %% Connection: close - orelse get_header_value("connection") =:= "close" + orelse is_close(get_header_value("connection", THIS)) %% HTTP 1.0 requires Connection: Keep-Alive orelse (Version =:= {1, 0} - andalso get_header_value("connection") =/= "Keep-Alive") + andalso get_header_value("connection", THIS) =/= "Keep-Alive") %% unread data left on the socket, can't safely continue orelse (DidNotRecv - andalso get_header_value("content-length") =/= undefined - andalso list_to_integer(get_header_value("content-length")) > 0) + andalso get_combined_header_value("content-length", THIS) =/= undefined + andalso list_to_integer(get_combined_header_value("content-length", THIS)) > 0) orelse (DidNotRecv - andalso get_header_value("transfer-encoding") =:= "chunked"). + andalso get_header_value("transfer-encoding", THIS) =:= "chunked"). -%% @spec cleanup() -> ok +is_close("close") -> + true; +is_close(S=[_C, _L, _O, _S, _E]) -> + string:to_lower(S) =:= "close"; +is_close(_) -> + false. + +%% @spec cleanup(request()) -> ok %% @doc Clean up any junk in the process dictionary, required before continuing %% a Keep-Alive request. -cleanup() -> - [erase(K) || K <- [?SAVE_QS, - ?SAVE_PATH, - ?SAVE_RECV, - ?SAVE_BODY, - ?SAVE_BODY_LENGTH, - ?SAVE_POST, - ?SAVE_COOKIE, - ?SAVE_FORCE_CLOSE]], +cleanup({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}) -> + L = [?SAVE_QS, ?SAVE_PATH, ?SAVE_RECV, ?SAVE_BODY, ?SAVE_BODY_LENGTH, + ?SAVE_POST, ?SAVE_COOKIE, ?SAVE_FORCE_CLOSE], + lists:foreach(fun(K) -> + erase(K) + end, L), ok. -%% @spec parse_qs() -> [{Key::string(), Value::string()}] +%% @spec parse_qs(request()) -> [{Key::string(), Value::string()}] %% @doc Parse the query string of the URL. -parse_qs() -> +parse_qs({?MODULE, [_Socket, _Method, RawPath, _Version, _Headers]}) -> case erlang:get(?SAVE_QS) of undefined -> {_, QueryString, _} = mochiweb_util:urlsplit_path(RawPath), @@ -424,17 +440,17 @@ parse_qs() -> Cached end. -%% @spec get_cookie_value(Key::string) -> string() | undefined +%% @spec get_cookie_value(Key::string, request()) -> string() | undefined %% @doc Get the value of the given cookie. -get_cookie_value(Key) -> - proplists:get_value(Key, parse_cookie()). +get_cookie_value(Key, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + proplists:get_value(Key, parse_cookie(THIS)). -%% @spec parse_cookie() -> [{Key::string(), Value::string()}] +%% @spec parse_cookie(request()) -> [{Key::string(), Value::string()}] %% @doc Parse the cookie header. -parse_cookie() -> +parse_cookie({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> case erlang:get(?SAVE_COOKIE) of undefined -> - Cookies = case get_header_value("cookie") of + Cookies = case get_header_value("cookie", THIS) of undefined -> []; Value -> @@ -446,17 +462,17 @@ parse_cookie() -> Cached end. -%% @spec parse_post() -> [{Key::string(), Value::string()}] +%% @spec parse_post(request()) -> [{Key::string(), Value::string()}] %% @doc Parse an application/x-www-form-urlencoded form POST. This %% has the side-effect of calling recv_body(). -parse_post() -> +parse_post({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> case erlang:get(?SAVE_POST) of undefined -> - Parsed = case recv_body() of + Parsed = case recv_body(THIS) of undefined -> []; Binary -> - case get_primary_header_value("content-type") of + case get_primary_header_value("content-type",THIS) of "application/x-www-form-urlencoded" ++ _ -> mochiweb_util:parse_qs(Binary); _ -> @@ -469,41 +485,43 @@ parse_post() -> Cached end. -%% @spec stream_chunked_body(integer(), fun(), term()) -> term() +%% @spec stream_chunked_body(integer(), fun(), term(), request()) -> term() %% @doc The function is called for each chunk. %% Used internally by read_chunked_body. -stream_chunked_body(MaxChunkSize, Fun, FunState) -> - case read_chunk_length() of +stream_chunked_body(MaxChunkSize, Fun, FunState, + {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + case read_chunk_length(THIS) of 0 -> - Fun({0, read_chunk(0)}, FunState); + Fun({0, read_chunk(0, THIS)}, FunState); Length when Length > MaxChunkSize -> - NewState = read_sub_chunks(Length, MaxChunkSize, Fun, FunState), - stream_chunked_body(MaxChunkSize, Fun, NewState); + NewState = read_sub_chunks(Length, MaxChunkSize, Fun, FunState, THIS), + stream_chunked_body(MaxChunkSize, Fun, NewState, THIS); Length -> - NewState = Fun({Length, read_chunk(Length)}, FunState), - stream_chunked_body(MaxChunkSize, Fun, NewState) + NewState = Fun({Length, read_chunk(Length, THIS)}, FunState), + stream_chunked_body(MaxChunkSize, Fun, NewState, THIS) end. -stream_unchunked_body(0, Fun, FunState) -> +stream_unchunked_body(0, Fun, FunState, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}) -> Fun({0, <<>>}, FunState); -stream_unchunked_body(Length, Fun, FunState) when Length > 0 -> +stream_unchunked_body(Length, Fun, FunState, + {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) when Length > 0 -> PktSize = case Length > ?RECBUF_SIZE of true -> ?RECBUF_SIZE; false -> Length end, - Bin = recv(PktSize), + Bin = recv(PktSize, THIS), NewState = Fun({PktSize, Bin}, FunState), - stream_unchunked_body(Length - PktSize, Fun, NewState). + stream_unchunked_body(Length - PktSize, Fun, NewState, THIS). -%% @spec read_chunk_length() -> integer() +%% @spec read_chunk_length(request()) -> integer() %% @doc Read the length of the next HTTP chunk. -read_chunk_length() -> - mochiweb_socket:setopts(Socket, [{packet, line}]), +read_chunk_length({?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) -> + ok = mochiweb_socket:setopts(Socket, [{packet, line}]), case mochiweb_socket:recv(Socket, 0, ?IDLE_TIMEOUT) of {ok, Header} -> - mochiweb_socket:setopts(Socket, [{packet, raw}]), + ok = mochiweb_socket:setopts(Socket, [{packet, raw}]), Splitter = fun (C) -> C =/= $\r andalso C =/= $\n andalso C =/= $ end, @@ -513,11 +531,11 @@ read_chunk_length() -> exit(normal) end. -%% @spec read_chunk(integer()) -> Chunk::binary() | [Footer::binary()] +%% @spec read_chunk(integer(), request()) -> Chunk::binary() | [Footer::binary()] %% @doc Read in a HTTP chunk of the given length. If Length is 0, then read the %% HTTP footers (as a list of binaries, since they're nominal). -read_chunk(0) -> - mochiweb_socket:setopts(Socket, [{packet, line}]), +read_chunk(0, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) -> + ok = mochiweb_socket:setopts(Socket, [{packet, line}]), F = fun (F1, Acc) -> case mochiweb_socket:recv(Socket, 0, ?IDLE_TIMEOUT) of {ok, <<"\r\n">>} -> @@ -529,10 +547,10 @@ read_chunk(0) -> end end, Footers = F(F, []), - mochiweb_socket:setopts(Socket, [{packet, raw}]), + ok = mochiweb_socket:setopts(Socket, [{packet, raw}]), put(?SAVE_RECV, true), Footers; -read_chunk(Length) -> +read_chunk(Length, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) -> case mochiweb_socket:recv(Socket, 2 + Length, ?IDLE_TIMEOUT) of {ok, <<Chunk:Length/binary, "\r\n">>} -> Chunk; @@ -540,32 +558,34 @@ read_chunk(Length) -> exit(normal) end. -read_sub_chunks(Length, MaxChunkSize, Fun, FunState) when Length > MaxChunkSize -> - Bin = recv(MaxChunkSize), +read_sub_chunks(Length, MaxChunkSize, Fun, FunState, + {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) when Length > MaxChunkSize -> + Bin = recv(MaxChunkSize, THIS), NewState = Fun({size(Bin), Bin}, FunState), - read_sub_chunks(Length - MaxChunkSize, MaxChunkSize, Fun, NewState); + read_sub_chunks(Length - MaxChunkSize, MaxChunkSize, Fun, NewState, THIS); -read_sub_chunks(Length, _MaxChunkSize, Fun, FunState) -> - Fun({Length, read_chunk(Length)}, FunState). +read_sub_chunks(Length, _MaxChunkSize, Fun, FunState, + {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + Fun({Length, read_chunk(Length, THIS)}, FunState). -%% @spec serve_file(Path, DocRoot) -> Response +%% @spec serve_file(Path, DocRoot, request()) -> Response %% @doc Serve a file relative to DocRoot. -serve_file(Path, DocRoot) -> - serve_file(Path, DocRoot, []). +serve_file(Path, DocRoot, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + serve_file(Path, DocRoot, [], THIS). -%% @spec serve_file(Path, DocRoot, ExtraHeaders) -> Response +%% @spec serve_file(Path, DocRoot, ExtraHeaders, request()) -> Response %% @doc Serve a file relative to DocRoot. -serve_file(Path, DocRoot, ExtraHeaders) -> +serve_file(Path, DocRoot, ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> case mochiweb_util:safe_relative_path(Path) of undefined -> - not_found(ExtraHeaders); + not_found(ExtraHeaders, THIS); RelPath -> FullPath = filename:join([DocRoot, RelPath]), case filelib:is_dir(FullPath) of true -> - maybe_redirect(RelPath, FullPath, ExtraHeaders); + maybe_redirect(RelPath, FullPath, ExtraHeaders, THIS); false -> - maybe_serve_file(FullPath, ExtraHeaders) + maybe_serve_file(FullPath, ExtraHeaders, THIS) end end. @@ -575,13 +595,14 @@ serve_file(Path, DocRoot, ExtraHeaders) -> directory_index(FullPath) -> filename:join([FullPath, "index.html"]). -maybe_redirect([], FullPath, ExtraHeaders) -> - maybe_serve_file(directory_index(FullPath), ExtraHeaders); +maybe_redirect([], FullPath, ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + maybe_serve_file(directory_index(FullPath), ExtraHeaders, THIS); -maybe_redirect(RelPath, FullPath, ExtraHeaders) -> +maybe_redirect(RelPath, FullPath, ExtraHeaders, + {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}=THIS) -> case string:right(RelPath, 1) of "/" -> - maybe_serve_file(directory_index(FullPath), ExtraHeaders); + maybe_serve_file(directory_index(FullPath), ExtraHeaders, THIS); _ -> Host = mochiweb_headers:get_value("host", Headers), Location = "http://" ++ Host ++ "/" ++ RelPath ++ "/", @@ -596,16 +617,16 @@ maybe_redirect(RelPath, FullPath, ExtraHeaders) -> "<p>The document has moved <a href=\"">>, Bottom = <<">here</a>.</p></body></html>\n">>, Body = <<Top/binary, LocationBin/binary, Bottom/binary>>, - respond({301, MoreHeaders, Body}) + respond({301, MoreHeaders, Body}, THIS) end. -maybe_serve_file(File, ExtraHeaders) -> - case read_file_info(File) of +maybe_serve_file(File, ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + case file:read_file_info(File) of {ok, FileInfo} -> - LastModified = couch_util:rfc1123_date(FileInfo#file_info.mtime), - case get_header_value("if-modified-since") of + LastModified = httpd_util:rfc1123_date(FileInfo#file_info.mtime), + case get_header_value("if-modified-since", THIS) of LastModified -> - respond({304, ExtraHeaders, ""}); + respond({304, ExtraHeaders, ""}, THIS); _ -> case file:open(File, [raw, binary]) of {ok, IoDevice} -> @@ -613,39 +634,20 @@ maybe_serve_file(File, ExtraHeaders) -> Res = ok({ContentType, [{"last-modified", LastModified} | ExtraHeaders], - {file, IoDevice}}), - file:close(IoDevice), + {file, IoDevice}}, THIS), + ok = file:close(IoDevice), Res; _ -> - not_found(ExtraHeaders) + not_found(ExtraHeaders, THIS) end end; {error, _} -> - not_found(ExtraHeaders) - end. - -read_file_info(File) -> - try - file:read_file_info(File, [{time, universal}]) - catch error:undef -> - case file:read_file_info(File) of - {ok, FileInfo} -> - {ok, FileInfo#file_info{ - atime=to_universal(FileInfo#file_info.atime), - mtime=to_universal(FileInfo#file_info.mtime), - ctime=to_universal(FileInfo#file_info.ctime) - }}; - Else -> - Else - end + not_found(ExtraHeaders, THIS) end. -to_universal(LocalTime) -> - erlang:localtime_to_universaltime(LocalTime). - server_headers() -> [{"Server", "MochiWeb/1.0 (" ++ ?QUIP ++ ")"}, - {"Date", couch_util:rfc1123_date()}]. + {"Date", httpd_util:rfc1123_date()}]. make_code(X) when is_integer(X) -> [integer_to_list(X), [" " | httpd_util:reason_phrase(X)]]; @@ -688,7 +690,7 @@ range_parts(Body0, Ranges) -> end, {lists:foldr(F, [], Ranges), Size}. -%% @spec accepted_encodings([encoding()]) -> [encoding()] | bad_accept_encoding_value +%% @spec accepted_encodings([encoding()], request()) -> [encoding()] | bad_accept_encoding_value %% @type encoding() = string(). %% %% @doc Returns a list of encodings accepted by a request. Encodings that are @@ -712,8 +714,8 @@ range_parts(Body0, Ranges) -> %% accepted_encodings(["gzip", "deflate", "identity"]) -> %% ["deflate", "gzip", "identity"] %% -accepted_encodings(SupportedEncodings) -> - AcceptEncodingHeader = case get_header_value("Accept-Encoding") of +accepted_encodings(SupportedEncodings, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + AcceptEncodingHeader = case get_header_value("Accept-Encoding", THIS) of undefined -> ""; Value -> @@ -728,7 +730,7 @@ accepted_encodings(SupportedEncodings) -> ) end. -%% @spec accepts_content_type(string() | binary()) -> boolean() | bad_accept_header +%% @spec accepts_content_type(string() | binary(), request()) -> boolean() | bad_accept_header %% %% @doc Determines whether a request accepts a given media type by analyzing its %% "Accept" header. @@ -750,16 +752,9 @@ accepted_encodings(SupportedEncodings) -> %% 5) For an "Accept" header with value "text/*; q=0.0, */*": %% accepts_content_type("text/plain") -> false %% -accepts_content_type(ContentType) when is_binary(ContentType) -> - accepts_content_type(binary_to_list(ContentType)); -accepts_content_type(ContentType1) -> +accepts_content_type(ContentType1, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> ContentType = re:replace(ContentType1, "\\s", "", [global, {return, list}]), - AcceptHeader = case get_header_value("Accept") of - undefined -> - "*/*"; - Value -> - Value - end, + AcceptHeader = accept_header(THIS), case mochiweb_util:parse_qvalues(AcceptHeader) of invalid_qvalue_string -> bad_accept_header; @@ -780,9 +775,83 @@ accepts_content_type(ContentType1) -> (not lists:member({SuperType, 0.0}, QList)) end. +%% @spec accepted_content_types([string() | binary()], request()) -> [string()] | bad_accept_header +%% +%% @doc Filters which of the given media types this request accepts. This filtering +%% is performed by analyzing the "Accept" header. The returned list is sorted +%% according to the preferences specified in the "Accept" header (higher Q values +%% first). If two or more types have the same preference (Q value), they're order +%% in the returned list is the same as they're order in the input list. +%% +%% Examples +%% +%% 1) For a missing "Accept" header: +%% accepted_content_types(["text/html", "application/json"]) -> +%% ["text/html", "application/json"] +%% +%% 2) For an "Accept" header with value "text/html, application/*": +%% accepted_content_types(["application/json", "text/html"]) -> +%% ["application/json", "text/html"] +%% +%% 3) For an "Accept" header with value "text/html, */*; q=0.0": +%% accepted_content_types(["text/html", "application/json"]) -> +%% ["text/html"] +%% +%% 4) For an "Accept" header with value "text/html; q=0.5, */*; q=0.1": +%% accepts_content_types(["application/json", "text/html"]) -> +%% ["text/html", "application/json"] +%% +accepted_content_types(Types1, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + Types = lists:map( + fun(T) -> re:replace(T, "\\s", "", [global, {return, list}]) end, + Types1), + AcceptHeader = accept_header(THIS), + case mochiweb_util:parse_qvalues(AcceptHeader) of + invalid_qvalue_string -> + bad_accept_header; + QList -> + TypesQ = lists:foldr( + fun(T, Acc) -> + case proplists:get_value(T, QList) of + undefined -> + [MainType, _SubType] = string:tokens(T, "/"), + case proplists:get_value(MainType ++ "/*", QList) of + undefined -> + case proplists:get_value("*/*", QList) of + Q when is_float(Q), Q > 0.0 -> + [{Q, T} | Acc]; + _ -> + Acc + end; + Q when Q > 0.0 -> + [{Q, T} | Acc]; + _ -> + Acc + end; + Q when Q > 0.0 -> + [{Q, T} | Acc]; + _ -> + Acc + end + end, + [], Types), + % Note: Stable sort. If 2 types have the same Q value we leave them in the + % same order as in the input list. + SortFun = fun({Q1, _}, {Q2, _}) -> Q1 >= Q2 end, + [Type || {_Q, Type} <- lists:sort(SortFun, TypesQ)] + end. + +accept_header({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + case get_header_value("Accept", THIS) of + undefined -> + "*/*"; + Value -> + Value + end. + %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). -endif. diff --git a/src/mochiweb/mochiweb_request_tests.erl b/src/mochiweb/mochiweb_request_tests.erl index b61a5839d..b40c867da 100644 --- a/src/mochiweb/mochiweb_request_tests.erl +++ b/src/mochiweb/mochiweb_request_tests.erl @@ -1,12 +1,13 @@ -module(mochiweb_request_tests). --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). accepts_content_type_test() -> Req1 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, mochiweb_headers:make([{"Accept", "multipart/related"}])), ?assertEqual(true, Req1:accepts_content_type("multipart/related")), + ?assertEqual(true, Req1:accepts_content_type(<<"multipart/related">>)), Req2 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, mochiweb_headers:make([{"Accept", "text/html"}])), @@ -60,4 +61,122 @@ accepts_content_type_test() -> mochiweb_headers:make([{"Accept", "text/html;level=1;q=0.1, text/html"}])), ?assertEqual(true, Req14:accepts_content_type("text/html; level=1")). +accepted_encodings_test() -> + Req1 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([])), + ?assertEqual(["identity"], + Req1:accepted_encodings(["gzip", "identity"])), + + Req2 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept-Encoding", "gzip, deflate"}])), + ?assertEqual(["gzip", "identity"], + Req2:accepted_encodings(["gzip", "identity"])), + + Req3 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept-Encoding", "gzip;q=0.5, deflate"}])), + ?assertEqual(["deflate", "gzip", "identity"], + Req3:accepted_encodings(["gzip", "deflate", "identity"])), + + Req4 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept-Encoding", "identity, *;q=0"}])), + ?assertEqual(["identity"], + Req4:accepted_encodings(["gzip", "deflate", "identity"])), + + Req5 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept-Encoding", "gzip; q=0.1, *;q=0"}])), + ?assertEqual(["gzip"], + Req5:accepted_encodings(["gzip", "deflate", "identity"])), + + Req6 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept-Encoding", "gzip; q=, *;q=0"}])), + ?assertEqual(bad_accept_encoding_value, + Req6:accepted_encodings(["gzip", "deflate", "identity"])), + + Req7 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept-Encoding", "gzip;q=2.0, *;q=0"}])), + ?assertEqual(bad_accept_encoding_value, + Req7:accepted_encodings(["gzip", "identity"])), + + Req8 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept-Encoding", "deflate, *;q=0.0"}])), + ?assertEqual([], + Req8:accepted_encodings(["gzip", "identity"])). + +accepted_content_types_test() -> + Req1 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/html"}])), + ?assertEqual(["text/html"], + Req1:accepted_content_types(["text/html", "application/json"])), + + Req2 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/html, */*;q=0"}])), + ?assertEqual(["text/html"], + Req2:accepted_content_types(["text/html", "application/json"])), + + Req3 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/*, */*;q=0"}])), + ?assertEqual(["text/html"], + Req3:accepted_content_types(["text/html", "application/json"])), + + Req4 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/*;q=0.8, */*;q=0.5"}])), + ?assertEqual(["text/html", "application/json"], + Req4:accepted_content_types(["application/json", "text/html"])), + + Req5 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/*;q=0.8, */*;q=0.5"}])), + ?assertEqual(["text/html", "application/json"], + Req5:accepted_content_types(["text/html", "application/json"])), + + Req6 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/*;q=0.5, */*;q=0.5"}])), + ?assertEqual(["application/json", "text/html"], + Req6:accepted_content_types(["application/json", "text/html"])), + + Req7 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make( + [{"Accept", "text/html;q=0.5, application/json;q=0.5"}])), + ?assertEqual(["application/json", "text/html"], + Req7:accepted_content_types(["application/json", "text/html"])), + + Req8 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/html"}])), + ?assertEqual([], + Req8:accepted_content_types(["application/json"])), + + Req9 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/*;q=0.9, text/html;q=0.5, */*;q=0.7"}])), + ?assertEqual(["application/json", "text/html"], + Req9:accepted_content_types(["text/html", "application/json"])). + +should_close_test() -> + F = fun (V, H) -> + (mochiweb_request:new( + nil, 'GET', "/", V, + mochiweb_headers:make(H) + )):should_close() + end, + ?assertEqual( + true, + F({1, 1}, [{"Connection", "close"}])), + ?assertEqual( + true, + F({1, 0}, [{"Connection", "close"}])), + ?assertEqual( + true, + F({1, 1}, [{"Connection", "ClOSe"}])), + ?assertEqual( + false, + F({1, 1}, [{"Connection", "closer"}])), + ?assertEqual( + false, + F({1, 1}, [])), + ?assertEqual( + true, + F({1, 0}, [])), + ?assertEqual( + false, + F({1, 0}, [{"Connection", "Keep-Alive"}])), + ok. + -endif. diff --git a/src/mochiweb/mochiweb_response.erl b/src/mochiweb/mochiweb_response.erl index ab8ee61ce..c2a94d9e7 100644 --- a/src/mochiweb/mochiweb_response.erl +++ b/src/mochiweb/mochiweb_response.erl @@ -3,39 +3,47 @@ %% @doc Response abstraction. --module(mochiweb_response, [Request, Code, Headers]). +-module(mochiweb_response). -author('bob@mochimedia.com'). -define(QUIP, "Any of you quaids got a smint?"). --export([get_header_value/1, get/1, dump/0]). --export([send/1, write_chunk/1]). +-export([new/3, get_header_value/2, get/2, dump/1]). +-export([send/2, write_chunk/2]). -%% @spec get_header_value(string() | atom() | binary()) -> string() | undefined +%% @type response() = {atom(), [Request, Code, Headers]} + +%% @spec new(Request, Code, Headers) -> response() +%% @doc Create a new mochiweb_response instance. +new(Request, Code, Headers) -> + {?MODULE, [Request, Code, Headers]}. + +%% @spec get_header_value(string() | atom() | binary(), response()) -> +%% string() | undefined %% @doc Get the value of the given response header. -get_header_value(K) -> +get_header_value(K, {?MODULE, [_Request, _Code, Headers]}) -> mochiweb_headers:get_value(K, Headers). -%% @spec get(request | code | headers) -> term() +%% @spec get(request | code | headers, response()) -> term() %% @doc Return the internal representation of the given field. -get(request) -> +get(request, {?MODULE, [Request, _Code, _Headers]}) -> Request; -get(code) -> +get(code, {?MODULE, [_Request, Code, _Headers]}) -> Code; -get(headers) -> +get(headers, {?MODULE, [_Request, _Code, Headers]}) -> Headers. -%% @spec dump() -> {mochiweb_request, [{atom(), term()}]} +%% @spec dump(response()) -> {mochiweb_request, [{atom(), term()}]} %% @doc Dump the internal representation to a "human readable" set of terms %% for debugging/inspection purposes. -dump() -> +dump({?MODULE, [Request, Code, Headers]}) -> [{request, Request:dump()}, {code, Code}, {headers, mochiweb_headers:to_list(Headers)}]. -%% @spec send(iodata()) -> ok +%% @spec send(iodata(), response()) -> ok %% @doc Send data over the socket if the method is not HEAD. -send(Data) -> +send(Data, {?MODULE, [Request, _Code, _Headers]}) -> case Request:get(method) of 'HEAD' -> ok; @@ -43,22 +51,22 @@ send(Data) -> Request:send(Data) end. -%% @spec write_chunk(iodata()) -> ok +%% @spec write_chunk(iodata(), response()) -> ok %% @doc Write a chunk of a HTTP chunked response. If Data is zero length, %% then the chunked response will be finished. -write_chunk(Data) -> +write_chunk(Data, {?MODULE, [Request, _Code, _Headers]}=THIS) -> case Request:get(version) of Version when Version >= {1, 1} -> Length = iolist_size(Data), - send([io_lib:format("~.16b\r\n", [Length]), Data, <<"\r\n">>]); + send([io_lib:format("~.16b\r\n", [Length]), Data, <<"\r\n">>], THIS); _ -> - send(Data) + send(Data, THIS) end. %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). -endif. diff --git a/src/mochiweb/mochiweb_socket.erl b/src/mochiweb/mochiweb_socket.erl index ad272048c..76b018c82 100644 --- a/src/mochiweb/mochiweb_socket.erl +++ b/src/mochiweb/mochiweb_socket.erl @@ -4,11 +4,10 @@ -module(mochiweb_socket). --export([listen/4, accept/1, after_accept/1, recv/3, send/2, close/1, port/1, peername/1, +-export([listen/4, accept/1, recv/3, send/2, close/1, port/1, peername/1, setopts/2, type/1]). -define(ACCEPT_TIMEOUT, 2000). --define(SSL_ACCEPT_TIMEOUT, 30000). listen(Ssl, Port, Opts, SslOpts) -> case Ssl of @@ -26,9 +25,14 @@ listen(Ssl, Port, Opts, SslOpts) -> accept({ssl, ListenSocket}) -> % There's a bug in ssl:transport_accept/2 at the moment, which is the % reason for the try...catch block. Should be fixed in OTP R14. - try ssl:transport_accept(ListenSocket, ?ACCEPT_TIMEOUT) of + try ssl:transport_accept(ListenSocket) of {ok, Socket} -> - {ok, {ssl, Socket}}; + case ssl:ssl_accept(Socket) of + ok -> + {ok, {ssl, Socket}}; + {error, _} = Err -> + Err + end; {error, _} = Err -> Err catch @@ -38,9 +42,6 @@ accept({ssl, ListenSocket}) -> accept(ListenSocket) -> gen_tcp:accept(ListenSocket, ?ACCEPT_TIMEOUT). -after_accept({ssl, Socket}) -> ssl:ssl_accept(Socket, ?SSL_ACCEPT_TIMEOUT); -after_accept(_Socket) -> ok. - recv({ssl, Socket}, Length, Timeout) -> ssl:recv(Socket, Length, Timeout); recv(Socket, Length, Timeout) -> diff --git a/src/mochiweb/mochiweb_socket_server.erl b/src/mochiweb/mochiweb_socket_server.erl index ff0d8f35a..029f1952d 100644 --- a/src/mochiweb/mochiweb_socket_server.erl +++ b/src/mochiweb/mochiweb_socket_server.erl @@ -9,7 +9,7 @@ -include("internal.hrl"). --export([start/1, stop/1]). +-export([start/1, start_link/1, stop/1]). -export([init/1, handle_call/3, handle_cast/2, terminate/2, code_change/3, handle_info/2]). -export([get/2, set/3]). @@ -33,10 +33,22 @@ -define(is_old_state(State), not is_record(State, mochiweb_socket_server)). -start(State=#mochiweb_socket_server{}) -> - start_server(State); +start_link(Options) -> + start_server(start_link, parse_options(Options)). + start(Options) -> - start(parse_options(Options)). + case lists:keytake(link, 1, Options) of + {value, {_Key, false}, Options1} -> + start_server(start, parse_options(Options1)); + _ -> + %% TODO: https://github.com/mochi/mochiweb/issues/58 + %% [X] Phase 1: Add new APIs (Sep 2011) + %% [_] Phase 2: Add deprecation warning + %% [_] Phase 3: Change default to {link, false} and ignore link + %% [_] Phase 4: Add deprecation warning for {link, _} option + %% [_] Phase 5: Remove support for {link, _} option + start_link(Options) + end. get(Name, Property) -> gen_server:call(Name, {get, Property}). @@ -61,6 +73,8 @@ stop(Options) -> %% Internal API +parse_options(State=#mochiweb_socket_server{}) -> + State; parse_options(Options) -> parse_options(Options, #mochiweb_socket_server{}). @@ -116,22 +130,22 @@ parse_options([{profile_fun, ProfileFun} | Rest], State) when is_function(Profil parse_options(Rest, State#mochiweb_socket_server{profile_fun=ProfileFun}). -start_server(State=#mochiweb_socket_server{ssl=Ssl, name=Name}) -> - case Ssl of - true -> - application:start(crypto), - application:start(public_key), - application:start(ssl); - false -> - void - end, +start_server(F, State=#mochiweb_socket_server{ssl=Ssl, name=Name}) -> + ok = prep_ssl(Ssl), case Name of undefined -> - gen_server:start_link(?MODULE, State, []); + gen_server:F(?MODULE, State, []); _ -> - gen_server:start_link(Name, ?MODULE, State, []) + gen_server:F(Name, ?MODULE, State, []) end. +prep_ssl(true) -> + ok = mochiweb:ensure_started(crypto), + ok = mochiweb:ensure_started(public_key), + ok = mochiweb:ensure_started(ssl); +prep_ssl(false) -> + ok. + ensure_int(N) when is_integer(N) -> N; ensure_int(S) when is_list(S) -> @@ -165,27 +179,7 @@ init(State=#mochiweb_socket_server{ip=Ip, port=Port, backlog=Backlog, nodelay=No {_, _, _, _, _, _, _, _} -> % IPv6 [inet6, {ip, Ip} | BaseOpts] end, - case listen(Port, Opts, State) of - {stop, eacces} -> - case Port < 1024 of - true -> - case catch fdsrv:start() of - {ok, _} -> - case fdsrv:bind_socket(tcp, Port) of - {ok, Fd} -> - listen(Port, [{fd, Fd} | Opts], State); - _ -> - {stop, fdsrv_bind_failed} - end; - _ -> - {stop, fdsrv_start_failed} - end; - false -> - {stop, eacces} - end; - Other -> - Other - end. + listen(Port, Opts, State). new_acceptor_pool(Listen, State=#mochiweb_socket_server{acceptor_pool=Pool, @@ -271,15 +265,8 @@ handle_cast(stop, State) -> terminate(Reason, State) when ?is_old_state(State) -> terminate(Reason, upgrade_state(State)); -terminate(_Reason, #mochiweb_socket_server{listen=Listen, port=Port}) -> - mochiweb_socket:close(Listen), - case Port < 1024 of - true -> - catch fdsrv:stop(), - ok; - false -> - ok - end. +terminate(_Reason, #mochiweb_socket_server{listen=Listen}) -> + mochiweb_socket:close(Listen). code_change(_OldVsn, State, _Extra) -> State. @@ -337,8 +324,8 @@ handle_info(Info, State) -> %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). upgrade_state_test() -> OldState = {mochiweb_socket_server, @@ -361,4 +348,3 @@ upgrade_state_test() -> ?assertEqual(CmpState, State). -endif. - diff --git a/src/mochiweb/mochiweb_util.erl b/src/mochiweb/mochiweb_util.erl index 6b8881894..4d399901e 100644 --- a/src/mochiweb/mochiweb_util.erl +++ b/src/mochiweb/mochiweb_util.erl @@ -9,7 +9,7 @@ -export([path_split/1]). -export([urlsplit/1, urlsplit_path/1, urlunsplit/1, urlunsplit_path/1]). -export([guess_mime/1, parse_header/1]). --export([shell_quote/1, cmd/1, cmd_string/1, cmd_port/2, cmd_status/1]). +-export([shell_quote/1, cmd/1, cmd_string/1, cmd_port/2, cmd_status/1, cmd_status/2]). -export([record_to_proplist/2, record_to_proplist/3]). -export([safe_relative_path/1, partition/2]). -export([parse_qvalues/1, pick_accepted_encodings/3]). @@ -124,11 +124,17 @@ cmd_string(Argv) -> string:join([shell_quote(X) || X <- Argv], " "). %% @spec cmd_status([string()]) -> {ExitStatus::integer(), Stdout::binary()} -%% @doc Accumulate the output and exit status from the given application, will be -%% spawned with cmd_port/2. +%% @doc Accumulate the output and exit status from the given application, +%% will be spawned with cmd_port/2. cmd_status(Argv) -> + cmd_status(Argv, []). + +%% @spec cmd_status([string()], [atom()]) -> {ExitStatus::integer(), Stdout::binary()} +%% @doc Accumulate the output and exit status from the given application, +%% will be spawned with cmd_port/2. +cmd_status(Argv, Options) -> Port = cmd_port(Argv, [exit_status, stderr_to_stdout, - use_stdio, binary]), + use_stdio, binary | Options]), try cmd_loop(Port, []) after catch port_close(Port) end. @@ -578,8 +584,8 @@ make_io(Io) when is_list(Io); is_binary(Io) -> %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). make_io_test() -> ?assertEqual( diff --git a/src/mochiweb/reloader.erl b/src/mochiweb/reloader.erl index c0f5de884..8266b338b 100644 --- a/src/mochiweb/reloader.erl +++ b/src/mochiweb/reloader.erl @@ -59,7 +59,7 @@ handle_cast(_Req, State) -> %% @doc gen_server callback. handle_info(doit, State) -> Now = stamp(), - doit(State#state.last, Now), + _ = doit(State#state.last, Now), {noreply, State#state{last = Now}}; handle_info(_Info, State) -> {noreply, State}. @@ -156,6 +156,6 @@ stamp() -> %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). -endif. |