couchdb-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From cml...@apache.org
Subject svn commit: r642953 [2/3] - in /incubator/couchdb/vendor/mochiweb: ./ current/ current/deps/ current/doc/ current/ebin/ current/include/ current/priv/ current/priv/skel/ current/priv/skel/deps/ current/priv/skel/doc/ current/priv/skel/ebin/ current/pri...
Date Mon, 31 Mar 2008 10:30:34 GMT
Added: incubator/couchdb/vendor/mochiweb/current/src/mochiweb_cookies.erl
URL: http://svn.apache.org/viewvc/incubator/couchdb/vendor/mochiweb/current/src/mochiweb_cookies.erl?rev=642953&view=auto
==============================================================================
--- incubator/couchdb/vendor/mochiweb/current/src/mochiweb_cookies.erl (added)
+++ incubator/couchdb/vendor/mochiweb/current/src/mochiweb_cookies.erl Mon Mar 31 03:30:27 2008
@@ -0,0 +1,250 @@
+%% @author Emad El-Haraty <emad@mochimedia.com>
+%% @copyright 2007 Mochi Media, Inc.
+
+%% @doc HTTP Cookie parsing and generating (RFC 2109, RFC 2965).
+
+-module(mochiweb_cookies).
+-export([parse_cookie/1, cookie/3, cookie/2, test/0]).
+
+-define(QUOTE, $\").
+
+-define(IS_WHITESPACE(C),
+	(C =:= $\s orelse C =:= $\t orelse C =:= $\r orelse C =:= $\n)).
+
+%% RFC 2616 separators (called tspecials in RFC 2068)
+-define(IS_SEPARATOR(C),
+	(C < 32 orelse
+	 C =:= $\s orelse C =:= $\t orelse
+	 C =:= $( orelse C =:= $) orelse C =:= $< orelse C =:= $> orelse
+	 C =:= $@ orelse C =:= $, orelse C =:= $; orelse C =:= $: orelse
+	 C =:= $\\ orelse C =:= $\" orelse C =:= $/ orelse
+         C =:= $[ orelse C =:= $] orelse C =:= $? orelse C =:= $= orelse
+         C =:= ${ orelse C =:= $})).
+
+%% @type proplist() = [{Key::string(), Value::string()}].
+%% @type header() = {Name::string(), Value::string()}.
+
+%% @spec cookie(Key::string(), Value::string()) -> header()
+%% @doc Short-hand for <code>cookie(Key, Value, [])</code>.
+cookie(Key, Value) ->
+    cookie(Key, Value, []).
+
+%% @spec cookie(Key::string(), Value::string(), Options::[Option]) -> header() 
+%% where Option = {max_age, integer()} | {local_time, {date(), time()}} 
+%%                | {domain, string()} | {path, string()}
+%%                | {secure, true | false}
+%%
+%% @doc Generate a Set-Cookie header field tuple.
+cookie(Key, Value, Options) ->
+    Cookie = [any_to_list(Key), "=", quote(Value), "; Version=1"],
+    %% Set-Cookie:
+    %%    Comment, Domain, Max-Age, Path, Secure, Version
+    %% Set-Cookie2:
+    %%    Comment, CommentURL, Discard, Domain, Max-Age, Path, Port, Secure,
+    %%    Version
+    ExpiresPart =
+        case proplists:get_value(max_age, Options) of
+            undefined ->
+                "";
+            RawAge ->
+                When = case proplists:get_value(local_time, Options) of
+                           undefined ->
+                               calendar:local_time();
+                           LocalTime ->
+                               LocalTime
+                       end,
+                Age = case RawAge < 0 of
+                          true ->
+                              0;
+                          false ->
+                              RawAge
+                      end,
+                ["; Expires=", age_to_cookie_date(Age, When),
+                 "; Max-Age=", quote(Age)]
+        end,
+    SecurePart =
+        case proplists:get_value(secure, Options) of
+            true ->
+                "; Secure";
+            _ ->
+                ""
+        end,
+    DomainPart =
+        case proplists:get_value(domain, Options) of
+            undefined ->
+                "";
+            Domain ->
+                ["; Domain=", quote(Domain)]
+        end,
+    PathPart =
+        case proplists:get_value(path, Options) of
+            undefined ->
+                "";
+            Path ->
+                ["; Path=", quote(Path)]
+        end,
+    CookieParts = [Cookie, ExpiresPart, SecurePart, DomainPart, PathPart],
+    {"Set-Cookie", lists:flatten(CookieParts)}.
+
+
+%% Every major browser incorrectly handles quoted strings in a
+%% different and (worse) incompatible manner.  Instead of wasting time
+%% writing redundant code for each browser, we restrict cookies to
+%% only contain characters that browsers handle compatibly.
+%%
+%% By replacing the definition of quote with this, we generate
+%% RFC-compliant cookies:
+%%
+%%     quote(V) ->
+%%         Fun = fun(?QUOTE, Acc) -> [$\\, ?QUOTE | Acc];
+%%                  (Ch, Acc) -> [Ch | Acc]
+%%               end,
+%%         [?QUOTE | lists:foldr(Fun, [?QUOTE], V)].
+
+%% Convert to a string and raise an error if quoting is required.
+quote(V0) ->
+    V = any_to_list(V0),
+    lists:all(fun(Ch) -> Ch =:= $/ orelse not ?IS_SEPARATOR(Ch) end, V)
+        orelse erlang:error({cookie_quoting_required, V}),
+    V.
+
+add_seconds(Secs, LocalTime) ->
+    Greg = calendar:datetime_to_gregorian_seconds(LocalTime),
+    calendar:gregorian_seconds_to_datetime(Greg + Secs).
+
+age_to_cookie_date(Age, LocalTime) ->
+    httpd_util:rfc1123_date(add_seconds(Age, LocalTime)).
+
+%% @spec parse_cookie(string()) -> [{K::string(), V::string()}]
+%% @doc Parse the contents of a Cookie header field, ignoring cookie
+%% attributes, and return a simple property list.
+parse_cookie("") -> 
+    [];
+parse_cookie(Cookie) -> 
+    parse_cookie(Cookie, []).
+
+%% @spec test() -> ok
+%% @doc Run tests for mochiweb_cookies.
+test() ->
+    parse_cookie_test(),
+    cookie_test(),
+    ok.
+
+%% Internal API
+
+parse_cookie([], Acc) ->
+    lists:reverse(Acc); 
+parse_cookie(String, Acc) -> 
+    {{Token, Value}, Rest} = read_pair(String),
+    Acc1 = case Token of
+	       "" ->
+		   Acc;
+	       "$" ++ _ ->
+		   Acc;
+	       _ ->
+		   [{Token, Value} | Acc]
+	   end,
+    parse_cookie(Rest, Acc1).
+
+read_pair(String) ->
+    {Token, Rest} = read_token(skip_whitespace(String)),
+    {Value, Rest1} = read_value(skip_whitespace(Rest)),
+    {{Token, Value}, skip_past_separator(Rest1)}.
+
+read_value([$= | Value]) ->
+    Value1 = skip_whitespace(Value),
+    case Value1 of
+	[?QUOTE | _] ->
+	    read_quoted(Value1);
+	_ ->
+	    read_token(Value1)
+    end;
+read_value(String) ->
+    {"", String}.
+
+read_quoted([?QUOTE | String]) ->
+    read_quoted(String, []).
+
+read_quoted([], Acc) ->
+    {lists:reverse(Acc), []};
+read_quoted([?QUOTE | Rest], Acc) ->
+    {lists:reverse(Acc), Rest};
+read_quoted([$\\, Any | Rest], Acc) ->
+    read_quoted(Rest, [Any | Acc]);
+read_quoted([C | Rest], Acc) ->
+    read_quoted(Rest, [C | Acc]).
+    
+skip_whitespace(String) ->
+    F = fun (C) -> ?IS_WHITESPACE(C) end,
+    lists:dropwhile(F, String).
+
+read_token(String) ->
+    F = fun (C) -> not ?IS_SEPARATOR(C) end,
+    lists:splitwith(F, String).
+
+skip_past_separator([]) ->    
+    [];
+skip_past_separator([$; | Rest]) ->
+    Rest;
+skip_past_separator([$, | Rest]) ->
+    Rest;
+skip_past_separator([_ | Rest]) ->
+    skip_past_separator(Rest).
+
+parse_cookie_test() ->
+    %% RFC example
+    C1 = "$Version=\"1\"; Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\"; 
+    Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\";
+    Shipping=\"FedEx\"; $Path=\"/acme\"",
+    [
+     {"Customer","WILE_E_COYOTE"},
+     {"Part_Number","Rocket_Launcher_0001"},
+     {"Shipping","FedEx"}
+    ] = parse_cookie(C1),
+    %% Potential edge cases
+    [{"foo", "x"}] = parse_cookie("foo=\"\\x\""),
+    [] = parse_cookie("="),
+    [{"foo", ""}, {"bar", ""}] = parse_cookie("  foo ; bar  "),
+    [{"foo", ""}, {"bar", ""}] = parse_cookie("foo=;bar="),
+    [{"foo", "\";"}, {"bar", ""}] = parse_cookie("foo = \"\\\";\";bar "),
+    [{"foo", "\";bar"}] = parse_cookie("foo=\"\\\";bar").
+
+any_to_list(V) when is_list(V) ->
+    V;
+any_to_list(V) when is_atom(V) ->
+    atom_to_list(V);
+any_to_list(V) when is_binary(V) ->
+    binary_to_list(V);
+any_to_list(V) when is_integer(V) ->
+    integer_to_list(V).
+
+
+cookie_test() ->
+    C1 = {"Set-Cookie",
+	  "Customer=WILE_E_COYOTE; "
+	  "Version=1; "
+	  "Path=/acme"},
+    C1 = cookie("Customer", "WILE_E_COYOTE", [{path, "/acme"}]),
+    C1 = cookie("Customer", "WILE_E_COYOTE",
+		[{path, "/acme"}, {badoption, "negatory"}]),
+    C1 = cookie('Customer', 'WILE_E_COYOTE', [{path, '/acme'}]),
+    C1 = cookie(<<"Customer">>, <<"WILE_E_COYOTE">>, [{path, <<"/acme">>}]),
+
+    {"Set-Cookie","=NoKey; Version=1"} = cookie("", "NoKey", []),
+	
+	LocalTime = calendar:universal_time_to_local_time({{2007, 5, 15}, {13, 45, 33}}), 
+    C2 = {"Set-Cookie",
+	  "Customer=WILE_E_COYOTE; "
+	  "Version=1; "
+	  "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; "
+	  "Max-Age=86417"},
+    C3 = cookie("Customer", "WILE_E_COYOTE",
+		[{max_age, 86417}, {local_time, LocalTime}]),
+    ok.

Added: incubator/couchdb/vendor/mochiweb/current/src/mochiweb_echo.erl
URL: http://svn.apache.org/viewvc/incubator/couchdb/vendor/mochiweb/current/src/mochiweb_echo.erl?rev=642953&view=auto
==============================================================================
--- incubator/couchdb/vendor/mochiweb/current/src/mochiweb_echo.erl (added)
+++ incubator/couchdb/vendor/mochiweb/current/src/mochiweb_echo.erl Mon Mar 31 03:30:27 2008
@@ -0,0 +1,31 @@
+%% @author Bob Ippolito <bob@mochimedia.com>
+%% @copyright 2007 Mochi Media, Inc.
+
+%% @doc Simple and stupid echo server to demo mochiweb_socket_server.
+
+-module(mochiweb_echo).
+-author('bob@mochimedia.com').
+-export([start/0, stop/0, loop/1]).
+
+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}}]).
+
+loop(Socket) ->
+    case gen_tcp:recv(Socket, 0, 30000) of
+	{ok, Data} ->
+	    case gen_tcp:send(Socket, Data) of
+		ok ->
+		    loop(Socket);
+		_ ->
+		    exit(normal)
+	    end;
+	_Other ->
+	    exit(normal)
+    end.

Added: incubator/couchdb/vendor/mochiweb/current/src/mochiweb_headers.erl
URL: http://svn.apache.org/viewvc/incubator/couchdb/vendor/mochiweb/current/src/mochiweb_headers.erl?rev=642953&view=auto
==============================================================================
--- incubator/couchdb/vendor/mochiweb/current/src/mochiweb_headers.erl (added)
+++ incubator/couchdb/vendor/mochiweb/current/src/mochiweb_headers.erl Mon Mar 31 03:30:27 2008
@@ -0,0 +1,178 @@
+%% @author Bob Ippolito <bob@mochimedia.com>
+%% @copyright 2007 Mochi Media, Inc.
+
+%% @doc Case preserving (but case insensitive) HTTP Header dictionary.
+
+-module(mochiweb_headers).
+-author('bob@mochimedia.com').
+-export([empty/0, from_list/1, insert/3, enter/3, get_value/2, lookup/2]).
+-export([get_primary_value/2]).
+-export([default/3, enter_from_list/2, default_from_list/2]).
+-export([to_list/1, make/1]).
+-export([test/0]).
+
+%% @type headers().
+%% @type key() = atom() | binary() | string().
+%% @type value() = atom() | binary() | string() | integer().
+
+%% @spec test() -> ok
+%% @doc Run tests for this module.
+test() ->
+    H = ?MODULE:make([{hdr, foo}, {"Hdr", "bar"}, {'Hdr', 2}]),
+    [{hdr, "foo, bar, 2"}] = ?MODULE:to_list(H), 
+    H1 = ?MODULE:insert(taco, grande, H),
+    [{hdr, "foo, bar, 2"}, {taco, "grande"}] = ?MODULE:to_list(H1),
+    H2 = ?MODULE:make([{"Set-Cookie", "foo"}]),
+    [{"Set-Cookie", "foo"}] = ?MODULE:to_list(H2),
+    H3 = ?MODULE:insert("Set-Cookie", "bar", H2),
+    [{"Set-Cookie", "foo"}, {"Set-Cookie", "bar"}] = ?MODULE:to_list(H3),
+    "foo, bar" = ?MODULE:get_value("set-cookie", H3),
+    {value, {"Set-Cookie", "foo, bar"}} = ?MODULE:lookup("set-cookie", H3),
+    undefined = ?MODULE:get_value("shibby", H3),
+    none = ?MODULE:lookup("shibby", H3),
+    H4 = ?MODULE:insert("content-type",
+                        "application/x-www-form-urlencoded; charset=utf8",
+                        H3),
+    "application/x-www-form-urlencoded" = ?MODULE:get_primary_value(
+                                             "content-type", H4),
+    ok.
+
+%% @spec empty() -> headers()
+%% @doc Create an empty headers structure.
+empty() ->
+    gb_trees:empty().
+
+%% @spec make(headers() | [{key(), value()}]) -> headers()
+%% @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) ->
+    T.
+
+%% @spec from_list([{key(), value()}]) -> headers()
+%% @doc Construct a headers() from the given list.
+from_list(List) ->
+    lists:foldl(fun ({K, V}, T) -> insert(K, V, T) end, empty(), List).
+
+%% @spec enter_from_list([{key(), value()}], headers()) -> headers()
+%% @doc Insert pairs into the headers, replace any values for existing keys.
+enter_from_list(List, T) ->
+    lists:foldl(fun ({K, V}, T1) -> enter(K, V, T1) end, T, List).
+
+%% @spec default_from_list([{key(), value()}], headers()) -> headers()
+%% @doc Insert pairs into the headers for keys that do not already exist.
+default_from_list(List, T) ->
+    lists:foldl(fun ({K, V}, T1) -> default(K, V, T1) end, T, List).
+
+%% @spec to_list(headers()) -> [{key(), string()}]
+%% @doc Return the contents of the headers. The keys will be the exact key
+%%      that was first inserted (e.g. may be an atom or binary, case is 
+%%      preserved).
+to_list(T) ->
+    F = fun ({K, {array, L}}, Acc) ->
+		L1 = lists:reverse(L),
+		lists:foldl(fun (V, Acc1) -> [{K, V} | Acc1] end, Acc, L1);
+	    (Pair, Acc) ->
+		[Pair | Acc]
+	end,
+    lists:reverse(lists:foldl(F, [], gb_trees:values(T))).
+
+%% @spec get_value(key(), headers()) -> string() | undefined
+%% @doc Return the value of the given header using a case insensitive search.
+%%      undefined will be returned for keys that are not present.
+get_value(K, T) ->
+    case lookup(K, T) of
+	{value, {_, V}} ->
+	    expand(V);
+	none ->
+	    undefined
+    end.
+
+%% @spec get_primary_value(key(), headers()) -> string() | undefined
+%% @doc Return the value of the given header up to the first semicolon using
+%%      a case insensitive search. undefined will be returned for keys
+%%      that are not present.
+get_primary_value(K, T) ->
+    case get_value(K, T) of
+        undefined ->
+            undefined;
+        V ->
+            lists:takewhile(fun (C) -> C =/= $; end, V)
+    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
+%%      not present.
+lookup(K, T) ->
+    case gb_trees:lookup(normalize(K), T) of
+	{value, {K0, V}} ->
+	    {value, {K0, expand(V)}};
+	none ->
+	    none
+    end.
+
+%% @spec default(key(), value(), headers()) -> headers()
+%% @doc Insert the pair into the headers if it does not already exist.
+default(K, V, T) ->
+    K1 = normalize(K),
+    V1 = any_to_list(V),
+    try gb_trees:insert(K1, {K, V1}, T)
+    catch
+	error:{key_exists, _} ->
+	    T
+    end.
+
+%% @spec enter(key(), value(), headers()) -> headers()
+%% @doc Insert the pair into the headers, replacing any pre-existing key.
+enter(K, V, T) ->
+    K1 = normalize(K),
+    V1 = any_to_list(V),
+    gb_trees:enter(K1, {K, V1}, T).
+
+%% @spec insert(key(), value(), headers()) -> headers()
+%% @doc Insert the pair into the headers, merging with any pre-existing key.
+%%      A merge is done with Value = V0 ++ ", " ++ V1.
+insert(K, V, T) ->
+    K1 = normalize(K),
+    V1 = any_to_list(V),
+    try gb_trees:insert(K1, {K, V1}, T)
+    catch
+	error:{key_exists, _} ->
+	    {K0, V0} = gb_trees:get(K1, T),
+	    V2 = merge(K1, V1, V0),
+	    gb_trees:update(K1, {K0, V2}, T)
+    end.
+
+%% Internal API
+
+expand({array, L}) ->
+    mochiweb_util:join(lists:reverse(L), ", ");
+expand(V) ->
+    V.
+
+merge("set-cookie", V1, {array, L}) ->
+    {array, [V1 | L]};
+merge("set-cookie", V1, V0) ->
+    {array, [V1, V0]};
+merge(_, V1, V0) ->
+    V0 ++ ", " ++ V1.
+
+normalize(K) when is_list(K) ->
+    string:to_lower(K);
+normalize(K) when is_atom(K) ->
+    normalize(atom_to_list(K));
+normalize(K) when is_binary(K) ->
+    normalize(binary_to_list(K)).
+
+any_to_list(V) when is_list(V) ->
+    V;
+any_to_list(V) when is_atom(V) ->
+    atom_to_list(V);
+any_to_list(V) when is_binary(V) ->
+    binary_to_list(V);
+any_to_list(V) when is_integer(V) ->
+    integer_to_list(V).
+
+

Added: incubator/couchdb/vendor/mochiweb/current/src/mochiweb_html.erl
URL: http://svn.apache.org/viewvc/incubator/couchdb/vendor/mochiweb/current/src/mochiweb_html.erl?rev=642953&view=auto
==============================================================================
--- incubator/couchdb/vendor/mochiweb/current/src/mochiweb_html.erl (added)
+++ incubator/couchdb/vendor/mochiweb/current/src/mochiweb_html.erl Mon Mar 31 03:30:27 2008
@@ -0,0 +1,760 @@
+%% @author Bob Ippolito <bob@mochimedia.com>
+%% @copyright 2007 Mochi Media, Inc.
+
+%% @doc Loosely tokenizes and generates parse trees for HTML 4.
+-module(mochiweb_html).
+-export([tokens/1, parse/1, parse_tokens/1, to_tokens/1, escape/1,
+         escape_attr/1, to_html/1, test/0]).
+
+% This is a macro to placate syntax highlighters..
+-define(QUOTE, $\").
+-define(SQUOTE, $\').
+-define(ADV_COL(S, N),
+        S#decoder{column=N+S#decoder.column,
+                  offset=N+S#decoder.offset}).
+-define(INC_COL(S),
+        S#decoder{column=1+S#decoder.column,
+                  offset=1+S#decoder.offset}).
+-define(INC_LINE(S),
+        S#decoder{column=1,
+                  line=1+S#decoder.line,
+                  offset=1+S#decoder.offset}).
+-define(INC_CHAR(S, C),
+        case C of
+            $\n ->
+                S#decoder{column=1,
+                          line=1+S#decoder.line,
+                          offset=1+S#decoder.offset};
+            _ ->
+                S#decoder{column=1+S#decoder.column,
+                          offset=1+S#decoder.offset}
+        end).
+
+-define(IS_WHITESPACE(C),
+ 	(C =:= $\s orelse C =:= $\t orelse C =:= $\r orelse C =:= $\n)).
+-define(IS_LITERAL_SAFE(C),
+        ((C >= $A andalso C =< $Z) orelse (C >= $a andalso C =< $z)
+         orelse (C >= $0 andalso C =< $9))).
+                                
+-record(decoder, {line=1,
+		  column=1,
+                  offset=0}).
+
+%% @type html_node() = {string(), [html_attr()], [html_node() | string()]}
+%% @type html_attr() = {string(), string()}
+%% @type html_token() = html_data() | start_tag() | end_tag() | inline_html() | html_comment() | html_doctype()
+%% @type html_data() = {data, string(), Whitespace::boolean()}
+%% @type start_tag() = {start_tag, Name, [html_attr()], Singleton::boolean()}
+%% @type end_tag() = {end_tag, Name}
+%% @type html_comment() = {comment, Comment}
+%% @type html_doctype() = {doctype, [Doctype]}
+%% @type inline_html() = {'=', iolist()}
+
+%% External API.
+
+%% @spec parse(string() | binary()) -> html_node()
+%% @doc tokenize and then transform the token stream into a HTML tree.
+parse(Input) ->
+    parse_tokens(tokens(Input)).
+
+%% @spec parse_tokens([html_token()]) -> html_node()
+%% @doc Transform the output of tokens(Doc) into a HTML tree.
+parse_tokens(Tokens) when is_list(Tokens) ->
+    %% Skip over doctype, processing instructions
+    F = fun (X) ->
+                case X of
+                    {start_tag, _, _, false} ->
+                        false;
+                    _ ->
+                        true
+                end
+        end,
+    [{start_tag, Tag, Attrs, false} | Rest] = lists:dropwhile(F, Tokens),
+    {Tree, _} = tree(Rest, [norm({Tag, Attrs})]),
+    Tree.
+
+%% @spec tokens(StringOrBinary) -> [html_token()]
+%% @doc Transform the input UTF-8 HTML into a token stream.
+tokens(Input) ->
+    tokens(iolist_to_binary(Input), #decoder{}, []).
+
+%% @spec to_tokens(html_node()) -> [html_token()]
+%% @doc Convert a html_node() tree to a list of tokens.
+to_tokens({Tag0}) ->
+    to_tokens({Tag0, [], []});
+to_tokens(T={'=', _}) ->
+    [T];
+to_tokens(T={doctype, _}) ->
+    [T];
+to_tokens(T={comment, _}) ->
+    [T];
+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)}]).
+
+%% @spec to_html([html_token()] | html_node()) -> iolist()
+%% @doc Convert a list of html_token() to a HTML document.
+to_html(Node) when is_tuple(Node) ->
+    to_html(to_tokens(Node));
+to_html(Tokens) when is_list(Tokens) ->
+    to_html(Tokens, []).
+
+%% @spec escape(string() | binary()) -> string()
+%% @doc Escape a string such that it's safe for HTML (amp; lt; gt;).
+escape(B) when is_binary(B) ->
+    escape(binary_to_list(B), []);
+escape(A) when is_atom(A) ->
+    escape(atom_to_list(A), []);
+escape(S) when is_list(S) ->
+    escape(S, []).
+
+%% @spec escape_attr(S::string()) -> string()
+%% @doc Escape a string such that it's safe for HTML attrs
+%%      (amp; lt; gt; quot;).
+escape_attr(B) when is_binary(B) ->
+    escape_attr(binary_to_list(B), []);
+escape_attr(A) when is_atom(A) ->
+    escape_attr(atom_to_list(A), []);
+escape_attr(S) when is_list(S) ->
+    escape_attr(S, []);
+escape_attr(I) when is_integer(I) ->
+    escape_attr(integer_to_list(I), []);
+escape_attr(F) when is_float(F) ->
+    escape_attr(mochinum:digits(F), []).
+
+%% @spec test() -> ok
+%% @doc Run tests for mochiweb_html.
+test() ->
+    test_destack(),
+    test_tokens(),
+    test_parse(),
+    test_parse_tokens(),
+    test_escape(),
+    test_escape_attr(),
+    test_to_html(),
+    ok.
+
+
+%% Internal API
+
+test_to_html() ->
+    Expect = <<"<html><head><title>hey!</title></head><body><p class=\"foo\">what's up<br /></p><div>sucka</div><!-- comment! --></body></html>">>,
+    Expect = iolist_to_binary(
+               to_html({html, [],
+                        [{<<"head">>, [],
+                          [{title, <<"hey!">>}]},
+                         {body, [],
+                          [{p, [{class, foo}], [<<"what's">>, <<" up">>, {br}]},
+                           {'div', <<"sucka">>},
+                           {comment, <<" comment! ">>}]}]})),
+    Expect1 = <<"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">">>,
+    Expect1 = iolist_to_binary(
+                to_html({doctype,
+                         [<<"html">>, <<"PUBLIC">>,
+                          <<"-//W3C//DTD XHTML 1.0 Transitional//EN">>,
+                          <<"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">>]})),
+    ok.
+to_html([], Acc) ->
+    lists:reverse(Acc);
+to_html([{'=', Content} | Rest], Acc) ->
+    to_html(Rest, [Content | Acc]);
+to_html([{pi, Tag, Attrs} | Rest], Acc) ->
+    Open = [<<"<?">>,
+            Tag,
+            attrs_to_html(Attrs, []),
+            <<"?>">>],
+    to_html(Rest, [Open | Acc]);
+to_html([{comment, Comment} | Rest], Acc) ->
+    to_html(Rest, [[<<"<!--">>, Comment, <<"-->">>] | Acc]);
+to_html([{doctype, Parts} | Rest], Acc) ->
+    Inside = doctype_to_html(Parts, Acc),
+    to_html(Rest, [[<<"<!DOCTYPE">>, Inside, <<">">>] | Acc]);
+to_html([{data, Data, _Whitespace} | Rest], Acc) ->
+    to_html(Rest, [escape(Data) | Acc]);
+to_html([{start_tag, Tag, Attrs, Singleton} | Rest], Acc) ->
+    Open = [<<"<">>,
+            Tag,
+            attrs_to_html(Attrs, []),
+            case Singleton of
+                true -> <<" />">>;
+                false -> <<">">>
+            end],
+    to_html(Rest, [Open | Acc]);
+to_html([{end_tag, Tag} | Rest], Acc) ->
+    to_html(Rest, [[<<"</">>, Tag, <<">">>] | Acc]).
+
+doctype_to_html([], Acc) ->
+    lists:reverse(Acc);
+doctype_to_html([Word | Rest], Acc) ->
+    case lists:all(fun (C) -> ?IS_LITERAL_SAFE(C) end,
+                   binary_to_list(iolist_to_binary(Word))) of
+        true ->
+            doctype_to_html(Rest, [[<<" ">>, Word] | Acc]);
+        false ->
+            doctype_to_html(Rest, [[<<" \"">>, escape_attr(Word), ?QUOTE] | Acc])
+    end.
+
+attrs_to_html([], Acc) ->
+    lists:reverse(Acc);
+attrs_to_html([{K, V} | Rest], Acc) ->
+    attrs_to_html(Rest,
+                  [[<<" ">>, escape(K), <<"=\"">>,
+                    escape_attr(V), <<"\"">>] | Acc]).
+    
+test_escape() ->
+    <<"&amp;quot;\"word &lt;&lt;up!&amp;quot;">> =
+        escape(<<"&quot;\"word <<up!&quot;">>),
+    ok.
+
+test_escape_attr() ->
+    <<"&amp;quot;&quot;word &lt;&lt;up!&amp;quot;">> =
+        escape_attr(<<"&quot;\"word <<up!&quot;">>),
+    ok.
+
+escape([], Acc) ->
+    list_to_binary(lists:reverse(Acc));
+escape("<" ++ Rest, Acc) ->
+    escape(Rest, lists:reverse("&lt;", Acc));
+escape(">" ++ Rest, Acc) ->
+    escape(Rest, lists:reverse("&gt;", Acc));
+escape("&" ++ Rest, Acc) ->
+    escape(Rest, lists:reverse("&amp;", Acc));
+escape([C | Rest], Acc) ->
+    escape(Rest, [C | Acc]).
+
+escape_attr([], Acc) ->
+    list_to_binary(lists:reverse(Acc));
+escape_attr("<" ++ Rest, Acc) ->
+    escape_attr(Rest, lists:reverse("&lt;", Acc));
+escape_attr(">" ++ Rest, Acc) ->
+    escape_attr(Rest, lists:reverse("&gt;", Acc));
+escape_attr("&" ++ Rest, Acc) ->
+    escape_attr(Rest, lists:reverse("&amp;", Acc));
+escape_attr([?QUOTE | Rest], Acc) ->
+    escape_attr(Rest, lists:reverse("&quot;", Acc));
+escape_attr([C | Rest], Acc) ->
+    escape_attr(Rest, [C | Acc]).
+
+to_tag(A) when is_atom(A) ->
+    norm(atom_to_list(A));
+to_tag(L) ->
+    norm(L).
+
+to_tokens([], Acc) ->
+    lists:reverse(Acc);
+to_tokens([{Tag, []} | Rest], Acc) ->
+    to_tokens(Rest, [{end_tag, to_tag(Tag)} | Acc]);
+to_tokens([{Tag0, [{T0} | R1]} | Rest], Acc) ->
+    %% Allow {br}
+    to_tokens([{Tag0, [{T0, [], []} | R1]} | Rest], Acc);
+to_tokens([{Tag0, [T0={'=', _C0} | R1]} | Rest], Acc) ->
+    %% Allow {'=', iolist()}
+    to_tokens([{Tag0, R1} | Rest], [T0 | Acc]);
+to_tokens([{Tag0, [T0={comment, _C0} | R1]} | Rest], Acc) ->
+    %% Allow {comment, iolist()}
+    to_tokens([{Tag0, R1} | Rest], [T0 | Acc]);
+to_tokens([{Tag0, [{T0, A0=[{_, _} | _]} | R1]} | Rest], Acc) ->
+    %% Allow {p, [{"class", "foo"}]}
+    to_tokens([{Tag0, [{T0, A0, []} | R1]} | Rest], Acc);
+to_tokens([{Tag0, [{T0, C0} | R1]} | Rest], Acc) ->
+    %% Allow {p, "content"} and {p, <<"content">>}
+    to_tokens([{Tag0, [{T0, [], C0} | R1]} | Rest], Acc);
+to_tokens([{Tag0, [{T0, A1, C0} | R1]} | Rest], Acc) when is_binary(C0) ->
+    %% Allow {"p", [{"class", "foo"}], <<"content">>}
+    to_tokens([{Tag0, [{T0, A1, binary_to_list(C0)} | R1]} | Rest], Acc);
+to_tokens([{Tag0, [{T0, A1, C0=[C | _]} | R1]} | Rest], Acc)
+  when is_integer(C) ->
+    %% Allow {"p", [{"class", "foo"}], "content"}
+    to_tokens([{Tag0, [{T0, A1, [C0]} | R1]} | Rest], Acc);
+to_tokens([{Tag0, [{T0, A1, C1} | R1]} | Rest], Acc) ->
+    %% Native {"p", [{"class", "foo"}], ["content"]}
+    Tag = to_tag(Tag0),
+    T1 = to_tag(T0),
+    case is_singleton(norm(T1)) of
+        true ->
+            to_tokens([{Tag, R1} | Rest], [{start_tag, T1, A1, true} | Acc]);
+        false ->
+            to_tokens([{T1, C1}, {Tag, R1} | Rest],
+                      [{start_tag, T1, A1, false} | Acc])
+    end;
+to_tokens([{Tag0, [L | R1]} | Rest], Acc) when is_list(L) ->
+    %% List text
+    Tag = to_tag(Tag0),
+    to_tokens([{Tag, R1} | Rest], [{data, iolist_to_binary(L), false} | Acc]);
+to_tokens([{Tag0, [B | R1]} | Rest], Acc) when is_binary(B) ->
+    %% Binary text
+    Tag = to_tag(Tag0),
+    to_tokens([{Tag, R1} | Rest], [{data, B, false} | Acc]).
+
+test_tokens() ->
+    [{start_tag, <<"foo">>, [{<<"bar">>, <<"baz">>},
+                             {<<"wibble">>, <<"wibble">>},
+                             {<<"alice">>, <<"bob">>}], true}] =
+        tokens(<<"<foo bar=baz wibble='wibble' alice=\"bob\"/>">>),
+    [{start_tag, <<"foo">>, [{<<"bar">>, <<"baz">>},
+                             {<<"wibble">>, <<"wibble">>},
+                             {<<"alice">>, <<"bob">>}], true}] =
+        tokens(<<"<foo bar=baz wibble='wibble' alice=bob/>">>),
+    [{comment, <<"[if lt IE 7]>\n<style type=\"text/css\">\n.no_ie { display: none; }\n</style>\n<![endif]">>}] =
+        tokens(<<"<!--[if lt IE 7]>\n<style type=\"text/css\">\n.no_ie { display: none; }\n</style>\n<![endif]-->">>),
+    ok.
+
+tokens(B, S=#decoder{offset=O}, Acc) ->
+    case B of
+        <<_:O/binary>> ->
+            lists:reverse(Acc);
+        _ ->
+            {Tag, S1} = tokenize(B, S),
+            tokens(B, S1, [Tag | Acc])
+    end.
+
+tokenize(B, S=#decoder{offset=O}) ->
+    case B of
+        <<_:O/binary, "<!--", _/binary>> ->
+            tokenize_comment(B, ?ADV_COL(S, 4));
+        <<_:O/binary, "<!DOCTYPE", _/binary>> ->
+            tokenize_doctype(B, ?ADV_COL(S, 10));
+        <<_:O/binary, "<![CDATA[", _/binary>> ->
+            tokenize_cdata(B, ?ADV_COL(S, 9));
+        <<_:O/binary, "<?", _/binary>> ->
+            {Tag, S1} = tokenize_literal(B, ?ADV_COL(S, 2)),
+            {Attrs, S2} = tokenize_attributes(B, S1),
+            S3 = find_qgt(B, S2),
+            {{pi, Tag, Attrs}, S3};
+        <<_:O/binary, "&", _/binary>> ->
+            tokenize_charref(B, ?INC_COL(S));
+        <<_:O/binary, "</", _/binary>> ->
+            {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) ->
+            %% This isn't really strict HTML but we want this for markdown
+            tokenize_data(B, ?INC_COL(S));
+        <<_:O/binary, "<", _/binary>> ->
+            {Tag, S1} = tokenize_literal(B, ?INC_COL(S)),
+            {Attrs, S2} = tokenize_attributes(B, S1),
+            {S3, HasSlash} = find_gt(B, S2),
+            Singleton = HasSlash orelse is_singleton(norm(binary_to_list(Tag))),
+            {{start_tag, Tag, Attrs, Singleton}, S3};
+        _ ->
+            tokenize_data(B, S)
+    end.
+
+test_parse() ->
+    D0 = <<"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">
+<html>
+ <head>
+   <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">
+   <title>Foo</title>
+   <link rel=\"stylesheet\" type=\"text/css\" href=\"/static/rel/dojo/resources/dojo.css\" media=\"screen\">
+   <link rel=\"stylesheet\" type=\"text/css\" href=\"/static/foo.css\" media=\"screen\">
+   <!--[if lt IE 7]>
+   <style type=\"text/css\">
+     .no_ie { display: none; }
+   </style>
+   <![endif]-->
+   <link rel=\"icon\" href=\"/static/images/favicon.ico\" type=\"image/x-icon\">
+   <link rel=\"shortcut icon\" href=\"/static/images/favicon.ico\" type=\"image/x-icon\">
+ </head>
+ <body id=\"home\" class=\"tundra\"><![CDATA[&lt;<this<!-- is -->CDATA>&gt;]]></body>
+</html>">>,
+    Expect = {<<"html">>, [],
+              [{<<"head">>, [],
+                [{<<"meta">>,
+                  [{<<"http-equiv">>,<<"Content-Type">>},
+                   {<<"content">>,<<"text/html; charset=UTF-8">>}],
+                  []},
+                 {<<"title">>,[],[<<"Foo">>]},
+                 {<<"link">>,
+                  [{<<"rel">>,<<"stylesheet">>},
+                   {<<"type">>,<<"text/css">>},
+                   {<<"href">>,<<"/static/rel/dojo/resources/dojo.css">>},
+                   {<<"media">>,<<"screen">>}],
+                  []},
+                 {<<"link">>,
+                  [{<<"rel">>,<<"stylesheet">>},
+                   {<<"type">>,<<"text/css">>},
+                   {<<"href">>,<<"/static/foo.css">>},
+                   {<<"media">>,<<"screen">>}],
+                  []},
+                 {comment,<<"[if lt IE 7]>\n   <style type=\"text/css\">\n     .no_ie { display: none; }\n   </style>\n   <![endif]">>},
+                 {<<"link">>,
+                  [{<<"rel">>,<<"icon">>},
+                   {<<"href">>,<<"/static/images/favicon.ico">>},
+                   {<<"type">>,<<"image/x-icon">>}],
+                  []},
+                 {<<"link">>,
+                  [{<<"rel">>,<<"shortcut icon">>},
+                   {<<"href">>,<<"/static/images/favicon.ico">>},
+                   {<<"type">>,<<"image/x-icon">>}],
+                  []}]},
+               {<<"body">>,
+                [{<<"id">>,<<"home">>},
+                 {<<"class">>,<<"tundra">>}],
+                [<<"&lt;<this<!-- is -->CDATA>&gt;">>]}]},
+    Expect = parse(D0),
+    ok.
+
+test_parse_tokens() ->
+    D0 = [{doctype,[<<"HTML">>,<<"PUBLIC">>,<<"-//W3C//DTD HTML 4.01 Transitional//EN">>]},
+          {data,<<"\n">>,true},
+          {start_tag,<<"html">>,[],false}],
+    {<<"html">>, [], []} = parse_tokens(D0),
+    D1 = D0 ++ [{end_tag, <<"html">>}],
+    {<<"html">>, [], []} = parse_tokens(D1),
+    D2 = D0 ++ [{start_tag, <<"body">>, [], false}],
+    {<<"html">>, [], [{<<"body">>, [], []}]} = parse_tokens(D2),
+    D3 = D0 ++ [{start_tag, <<"head">>, [], false},
+                {end_tag, <<"head">>},
+                {start_tag, <<"body">>, [], false}],
+    {<<"html">>, [], [{<<"head">>, [], []}, {<<"body">>, [], []}]} = parse_tokens(D3),
+    D4 = D3 ++ [{data,<<"\n">>,true},
+                {start_tag,<<"div">>,[{<<"class">>,<<"a">>}],false},
+                {start_tag,<<"a">>,[{<<"name">>,<<"#anchor">>}],false},
+                {end_tag,<<"a">>},
+                {end_tag,<<"div">>},
+                {start_tag,<<"div">>,[{<<"class">>,<<"b">>}],false},
+                {start_tag,<<"div">>,[{<<"class">>,<<"c">>}],false},
+                {end_tag,<<"div">>},
+                {end_tag,<<"div">>}],
+    {<<"html">>, [],
+     [{<<"head">>, [], []},
+      {<<"body">>, [],
+       [{<<"div">>, [{<<"class">>, <<"a">>}], [{<<"a">>, [{<<"name">>, <<"#anchor">>}], []}]},
+        {<<"div">>, [{<<"class">>, <<"b">>}], [{<<"div">>, [{<<"class">>, <<"c">>}], []}]}
+       ]}]} = parse_tokens(D4),
+    D5 = [{start_tag,<<"html">>,[],false},
+          {data,<<"\n">>,true},
+          {data,<<"boo">>,false},
+          {data,<<"hoo">>,false},
+          {data,<<"\n">>,true},
+          {end_tag,<<"html">>}],
+    {<<"html">>, [], [<<"\nboohoo\n">>]} = parse_tokens(D5),
+    D6 = [{start_tag,<<"html">>,[],false},
+          {data,<<"\n">>,true},
+          {data,<<"\n">>,true},
+          {end_tag,<<"html">>}],
+    {<<"html">>, [], []} = parse_tokens(D6),
+    D7 = [{start_tag,<<"html">>,[],false},
+          {start_tag,<<"ul">>,[],false},
+          {start_tag,<<"li">>,[],false},
+          {data,<<"word">>,false},
+          {start_tag,<<"li">>,[],false},
+          {data,<<"up">>,false},
+          {end_tag,<<"li">>},
+          {start_tag,<<"li">>,[],false},
+          {data,<<"fdsa">>,false},
+          {start_tag,<<"br">>,[],true},
+          {data,<<"asdf">>,false},
+          {end_tag,<<"ul">>},
+          {end_tag,<<"html">>}],
+    {<<"html">>, [],
+     [{<<"ul">>, [],
+       [{<<"li">>, [], [<<"word">>]},
+        {<<"li">>, [], [<<"up">>]},
+        {<<"li">>, [], [<<"fdsa">>,{<<"br">>, [], []}, <<"asdf">>]}]}]} = parse_tokens(D7),
+    ok.
+
+tree_data([{data, Data, Whitespace} | Rest], AllWhitespace, Acc) ->
+    tree_data(Rest, (Whitespace andalso AllWhitespace), [Data | Acc]);
+tree_data(Rest, AllWhitespace, Acc) ->
+    {iolist_to_binary(lists:reverse(Acc)), AllWhitespace, Rest}.
+
+tree([], Stack) ->
+    {destack(Stack), []};
+tree([{end_tag, Tag} | Rest], Stack) ->
+    case destack(norm(Tag), Stack) of
+        S when is_list(S) ->
+            tree(Rest, S);
+        Result ->
+            {Result, []}
+    end;
+tree([{start_tag, Tag, Attrs, true} | Rest], S) ->
+    tree(Rest, append_stack_child(norm({Tag, Attrs}), S));
+tree([{start_tag, Tag, Attrs, false} | Rest], S) ->
+    tree(Rest, stack(norm({Tag, Attrs}), S));
+tree([T={pi, _Tag, _Attrs} | Rest], S) ->
+    tree(Rest, append_stack_child(T, S));
+tree([T={comment, _Comment} | Rest], S) ->
+    tree(Rest, append_stack_child(T, S));
+tree(L=[{data, _Data, _Whitespace} | _], S) ->
+    case tree_data(L, true, []) of
+        {_, true, Rest} -> 
+            tree(Rest, S);
+        {Data, false, Rest} ->
+            tree(Rest, append_stack_child(Data, S))
+    end.
+
+norm({Tag, Attrs}) ->
+    {norm(Tag), [{norm(K), iolist_to_binary(V)} || {K, V} <- Attrs], []};
+norm(Tag) when is_binary(Tag) ->
+    Tag;
+norm(Tag) ->
+    list_to_binary(string:to_lower(Tag)).
+
+test_destack() ->
+    {<<"a">>, [], []} =
+        destack([{<<"a">>, [], []}]),
+    {<<"a">>, [], [{<<"b">>, [], []}]} =
+        destack([{<<"b">>, [], []}, {<<"a">>, [], []}]),
+    {<<"a">>, [], [{<<"b">>, [], [{<<"c">>, [], []}]}]} =
+     destack([{<<"c">>, [], []}, {<<"b">>, [], []}, {<<"a">>, [], []}]),
+    [{<<"a">>, [], [{<<"b">>, [], [{<<"c">>, [], []}]}]}] =
+     destack(<<"b">>,
+             [{<<"c">>, [], []}, {<<"b">>, [], []}, {<<"a">>, [], []}]),
+    [{<<"b">>, [], [{<<"c">>, [], []}]}, {<<"a">>, [], []}] =
+     destack(<<"c">>,
+             [{<<"c">>, [], []}, {<<"b">>, [], []},{<<"a">>, [], []}]),
+    ok.
+
+stack(T1={TN, _, _}, Stack=[{TN, _, _} | _Rest])
+  when TN =:= <<"li">> orelse TN =:= <<"option">> ->
+    [T1 | destack(TN, Stack)];
+stack(T1={TN0, _, _}, Stack=[{TN1, _, _} | _Rest])
+  when (TN0 =:= <<"dd">> orelse TN0 =:= <<"dt">>) andalso
+       (TN1 =:= <<"dd">> orelse TN1 =:= <<"dt">>) ->
+    [T1 | destack(TN1, Stack)];
+stack(T1, Stack) ->
+    [T1 | Stack].
+
+append_stack_child(StartTag, [{Name, Attrs, Acc} | Stack]) ->
+    [{Name, Attrs, [StartTag | Acc]} | Stack].
+
+destack(TagName, Stack) when is_list(Stack) ->
+    F = fun (X) ->
+                case X of 
+                    {TagName, _, _} ->
+                        false;
+                    _ ->
+                        true
+                end
+        end,
+    case lists:splitwith(F, Stack) of
+        {_, []} ->
+            %% No match, no state change
+            Stack;
+        {_Pre, [_T]} ->
+            %% Unfurl the whole stack, we're done
+            destack(Stack);
+        {Pre, [T, {T0, A0, Acc0} | Post]} ->
+            %% Unfurl up to the tag, then accumulate it
+            [{T0, A0, [destack(Pre ++ [T]) | Acc0]} | Post]
+    end.
+    
+destack([{Tag, Attrs, Acc}]) ->
+    {Tag, Attrs, lists:reverse(Acc)};
+destack([{T1, A1, Acc1}, {T0, A0, Acc0} | Rest]) ->
+    destack([{T0, A0, [{T1, A1, lists:reverse(Acc1)} | Acc0]} | Rest]).
+
+is_singleton(<<"br">>) -> true;
+is_singleton(<<"hr">>) -> true;
+is_singleton(<<"img">>) -> true;
+is_singleton(<<"input">>) -> true;
+is_singleton(<<"base">>) -> true;
+is_singleton(<<"meta">>) -> true;
+is_singleton(<<"link">>) -> true;
+is_singleton(<<"area">>) -> true;
+is_singleton(<<"param">>) -> true;
+is_singleton(<<"col">>) -> true;
+is_singleton(_) -> false.
+
+tokenize_data(B, S=#decoder{offset=O}) ->
+    tokenize_data(B, S, O, true).
+
+tokenize_data(B, S=#decoder{offset=O}, Start, Whitespace) ->
+    case B of
+        <<_:O/binary, C, _/binary>> when (C =/= $< andalso C =/= $&) ->
+            tokenize_data(B, ?INC_CHAR(S, C), Start,
+                          (Whitespace andalso ?IS_WHITESPACE(C)));
+        _ ->
+            Len = O - Start,
+            <<_:Start/binary, Data:Len/binary, _/binary>> = B,
+            {{data, Data, Whitespace}, S}
+    end.
+
+tokenize_attributes(B, S) ->
+    tokenize_attributes(B, S, []).
+
+tokenize_attributes(B, S=#decoder{offset=O}, Acc) ->
+    case B of
+        <<_:O/binary>> ->
+            {lists:reverse(Acc), S};
+        <<_:O/binary, C, _/binary>> when (C =:= $> orelse C =:= $/) ->
+            {lists:reverse(Acc), S};
+        <<_:O/binary, "?>", _/binary>> ->
+            {lists:reverse(Acc), S};
+        <<_:O/binary, C, _/binary>> when ?IS_WHITESPACE(C) ->
+            tokenize_attributes(B, ?INC_CHAR(S, C), Acc);
+        _ ->
+            {Attr, S1} = tokenize_literal(B, S),
+            {Value, S2} = tokenize_attr_value(Attr, B, S1),
+            tokenize_attributes(B, S2, [{Attr, Value} | Acc])
+    end.
+
+tokenize_attr_value(Attr, B, S) ->
+    S1 = skip_whitespace(B, S),
+    O = S1#decoder.offset,
+    case B of
+        <<_:O/binary, "=", _/binary>> ->
+            tokenize_word_or_literal(B, ?INC_COL(S1));
+        _ ->
+            {Attr, S1}
+    end.
+
+skip_whitespace(B, S=#decoder{offset=O}) ->
+    case B of
+        <<_:O/binary, C, _/binary>> when ?IS_WHITESPACE(C) ->
+            skip_whitespace(B, ?INC_CHAR(S, C));
+        _ ->
+            S
+    end.
+
+tokenize_literal(Bin, S) ->
+    tokenize_literal(Bin, S, []).
+
+tokenize_literal(Bin, S=#decoder{offset=O}, Acc) ->
+    case Bin of
+        <<_:O/binary, $&, _/binary>> ->
+            {{data, Data, false}, S1} = tokenize_charref(Bin, ?INC_COL(S)),
+            tokenize_literal(Bin, S1, [Data | Acc]);
+        <<_:O/binary, C, _/binary>> when not (?IS_WHITESPACE(C)
+                                              orelse C =:= $>
+                                              orelse C =:= $/
+                                              orelse C =:= $=) ->
+            tokenize_literal(Bin, ?INC_COL(S), [C | Acc]);
+        _ ->
+            {iolist_to_binary(lists:reverse(Acc)), S}
+    end.
+
+find_qgt(Bin, S=#decoder{offset=O}) ->
+    case Bin of
+        <<_:O/binary, "?>", _/binary>> ->
+            ?ADV_COL(S, 2);
+        <<_:O/binary, C, _/binary>> ->
+            find_qgt(Bin, ?INC_CHAR(S, C));
+        _ ->
+            S
+    end.
+
+find_gt(Bin, S) ->
+    find_gt(Bin, S, false).
+
+find_gt(Bin, S=#decoder{offset=O}, HasSlash) ->
+    case Bin of
+        <<_:O/binary, $/, _/binary>> ->
+            find_gt(Bin, ?INC_COL(S), true);
+        <<_:O/binary, $>, _/binary>> ->
+            {?INC_COL(S), HasSlash};
+        <<_:O/binary, C, _/binary>> ->
+            find_gt(Bin, ?INC_CHAR(S, C), HasSlash);
+        _ ->
+            {S, HasSlash}
+    end.
+
+tokenize_charref(Bin, S=#decoder{offset=O}) ->
+    tokenize_charref(Bin, S, O).
+    
+tokenize_charref(Bin, S=#decoder{offset=O}, Start) ->
+    case Bin of
+        <<_:O/binary>> ->
+            <<_:Start/binary, Raw/binary>> = Bin,
+            {{data, Raw, false}, S};
+        <<_: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};
+        <<_: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 ->
+                           list_to_binary(xmerl_ucs:to_utf8(Unichar))
+                   end,
+            {{data, Data, false}, ?INC_COL(S)};
+        _ ->
+            tokenize_charref(Bin, ?INC_COL(S), Start)
+    end.
+
+tokenize_doctype(Bin, S) ->
+    tokenize_doctype(Bin, S, []).
+
+tokenize_doctype(Bin, S=#decoder{offset=O}, Acc) ->
+    case Bin of
+        <<_:O/binary>> ->
+            {{doctype, lists:reverse(Acc)}, S};
+        <<_:O/binary, $>, _/binary>> ->
+            {{doctype, lists:reverse(Acc)}, ?INC_COL(S)};
+        <<_:O/binary, C, _/binary>> when ?IS_WHITESPACE(C) ->
+            tokenize_doctype(Bin, ?INC_CHAR(S, C), Acc);
+        _ ->
+            {Word, S1} = tokenize_word_or_literal(Bin, S),
+            tokenize_doctype(Bin, S1, [Word | Acc])
+    end.
+
+tokenize_word_or_literal(Bin, S=#decoder{offset=O}) ->
+    case Bin of
+        <<_:O/binary, C, _/binary>> when ?IS_WHITESPACE(C) ->
+            {error, {whitespace, [C], S}};
+        <<_:O/binary, C, _/binary>> when C =:= ?QUOTE orelse C =:= ?SQUOTE ->
+            tokenize_word(Bin, ?INC_COL(S), C);
+        _ ->
+            tokenize_literal(Bin, S, [])
+    end.
+
+tokenize_word(Bin, S, Quote) ->
+    tokenize_word(Bin, S, Quote, []).
+
+tokenize_word(Bin, S=#decoder{offset=O}, Quote, Acc) ->
+    case Bin of
+        <<_:O/binary>> ->
+            {iolist_to_binary(lists:reverse(Acc)), S};
+        <<_:O/binary, Quote, _/binary>> ->
+            {iolist_to_binary(lists:reverse(Acc)), ?INC_COL(S)};
+        <<_:O/binary, $&, _/binary>> ->
+            {{data, Data, false}, S1} = tokenize_charref(Bin, ?INC_COL(S)),
+            tokenize_word(Bin, S1, Quote, [Data | Acc]);
+        <<_:O/binary, C, _/binary>> ->
+            tokenize_word(Bin, ?INC_CHAR(S, C), Quote, [C | Acc])
+    end.
+
+tokenize_cdata(Bin, S=#decoder{offset=O}) ->
+    tokenize_cdata(Bin, S, O).
+
+tokenize_cdata(Bin, S=#decoder{offset=O}, Start) ->
+    case Bin of
+        <<_:O/binary, "]]>", _/binary>> ->
+            Len = O - Start,
+            <<_:Start/binary, Raw:Len/binary, _/binary>> = Bin,
+            {{data, Raw, false}, ?ADV_COL(S, 3)};
+        <<_:O/binary, C, _/binary>> ->
+            tokenize_cdata(Bin, ?INC_CHAR(S, C), Start);
+        _ ->
+            <<_:O/binary, Raw/binary>> = Bin,
+            {{data, Raw, false}, S}
+    end.
+
+tokenize_comment(Bin, S=#decoder{offset=O}) ->
+    tokenize_comment(Bin, S, O).
+
+tokenize_comment(Bin, S=#decoder{offset=O}, Start) ->
+    case Bin of
+        <<_:O/binary, "-->", _/binary>> ->
+            Len = O - Start,
+            <<_:Start/binary, Raw:Len/binary, _/binary>> = Bin,
+            {{comment, Raw}, ?ADV_COL(S, 3)};
+        <<_:O/binary, C, _/binary>> ->
+            tokenize_comment(Bin, ?INC_CHAR(S, C), Start);
+        <<_:Start/binary, Raw/binary>> ->
+            {{comment, Raw}, S}
+    end.

Added: incubator/couchdb/vendor/mochiweb/current/src/mochiweb_http.erl
URL: http://svn.apache.org/viewvc/incubator/couchdb/vendor/mochiweb/current/src/mochiweb_http.erl?rev=642953&view=auto
==============================================================================
--- incubator/couchdb/vendor/mochiweb/current/src/mochiweb_http.erl (added)
+++ incubator/couchdb/vendor/mochiweb/current/src/mochiweb_http.erl Mon Mar 31 03:30:27 2008
@@ -0,0 +1,132 @@
+%% @author Bob Ippolito <bob@mochimedia.com>
+%% @copyright 2007 Mochi Media, Inc.
+
+%% @doc HTTP server.
+
+-module(mochiweb_http).
+-author('bob@mochimedia.com').
+-export([start/0, start/1, stop/0, stop/1]).
+-export([loop/2, default_body/1]).
+
+-define(IDLE_TIMEOUT, 30000).
+
+-define(DEFAULTS, [{name, ?MODULE},
+		   {port, 8888}]).
+
+set_default({Prop, Value}, PropList) ->
+    case proplists:is_defined(Prop, PropList) of
+	true ->
+	    PropList;
+	false ->
+	    [{Prop, Value} | PropList]
+    end.
+
+set_defaults(Defaults, PropList) ->
+    lists:foldl(fun set_default/2, PropList, Defaults).
+
+parse_options(Options) ->
+    {loop, HttpLoop} = proplists:lookup(loop, Options),
+    Loop = fun (S) ->
+		   ?MODULE:loop(S, HttpLoop)
+	   end,
+    Options1 = [{loop, Loop} | proplists:delete(loop, Options)],
+    set_defaults(?DEFAULTS, Options1).
+
+stop() ->
+    mochiweb_socket_server:stop(?MODULE).
+
+stop(Name) ->
+    mochiweb_socket_server:stop(Name).
+    
+start() ->
+    start([{ip, "127.0.0.1"},
+	   {loop, {?MODULE, default_body}}]).
+
+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)).
+
+loop(Socket, Body) ->
+    inet:setopts(Socket, [{packet, http}]),
+    request(Socket, Body).
+
+request(Socket, Body) ->
+    case gen_tcp:recv(Socket, 0, ?IDLE_TIMEOUT) of
+	{ok, {http_request, Method, Path, Version}} ->
+	    headers(Socket, {Method, Path, Version}, [], Body);
+	{error, {http_error, "\r\n"}} ->
+	    request(Socket, Body);
+	{error, {http_error, "\n"}} ->
+	    request(Socket, Body);
+	_Other ->
+	    gen_tcp:close(Socket),
+	    exit(normal)
+    end.
+
+headers(Socket, Request, Headers, Body) ->
+    case gen_tcp:recv(Socket, 0, ?IDLE_TIMEOUT) of
+	{ok, http_eoh} ->
+	    inet:setopts(Socket, [{packet, raw}]),
+	    Req = mochiweb:new_request({Socket, Request,
+					lists:reverse(Headers)}),
+	    Body(Req),
+	    case Req:should_close() of
+		true ->
+		    gen_tcp:close(Socket),
+		    exit(normal);
+		false ->
+		    Req:cleanup(),
+		    ?MODULE:loop(Socket, Body)
+	    end;
+	{ok, {http_header, _, Name, _, Value}} ->
+	    headers(Socket, Request, [{Name, Value} | Headers], Body);
+	_Other ->
+	    gen_tcp:close(Socket),
+	    exit(normal)
+    end.

Added: incubator/couchdb/vendor/mochiweb/current/src/mochiweb_multipart.erl
URL: http://svn.apache.org/viewvc/incubator/couchdb/vendor/mochiweb/current/src/mochiweb_multipart.erl?rev=642953&view=auto
==============================================================================
--- incubator/couchdb/vendor/mochiweb/current/src/mochiweb_multipart.erl (added)
+++ incubator/couchdb/vendor/mochiweb/current/src/mochiweb_multipart.erl Mon Mar 31 03:30:27 2008
@@ -0,0 +1,428 @@
+%% @author Bob Ippolito <bob@mochimedia.com>
+%% @copyright 2007 Mochi Media, Inc.
+
+%% @doc Utilities for parsing multipart/form-data.
+
+-module(mochiweb_multipart).
+-author('bob@mochimedia.com').
+
+-export([parse_form/2]).
+-export([parse_multipart_request/2]).
+-export([test/0]).
+
+-define(CHUNKSIZE, 4096).
+
+-record(mp, {state, boundary, length, buffer, callback, req}).
+
+%% TODO: DOCUMENT THIS MODULE.
+
+parse_form(Req, FileHandler) ->
+    Callback = fun (Next) -> parse_form_outer(Next, FileHandler, []) end,
+    {_, _, Res} = parse_multipart_request(Req, Callback),
+    Res.
+
+parse_form_outer(eof, _, Acc) ->
+    lists:reverse(Acc);
+parse_form_outer({headers, H}, FileHandler, State) ->
+    {"form-data", H1} = proplists:get_value("content-disposition", H),
+    Name = proplists:get_value("name", H1),
+    Filename = proplists:get_value("filename", H1),
+    case Filename of
+        undefined ->
+            fun (Next) ->
+                    parse_form_value(Next, {Name, []}, FileHandler, State)
+            end;
+        _ ->
+            ContentType = proplists:get_value("content-type", H),
+            Handler = FileHandler(Filename, ContentType),
+            fun (Next) ->
+                    parse_form_file(Next, {Name, Handler}, FileHandler, State)
+            end
+    end.
+
+parse_form_value(body_end, {Name, Acc}, FileHandler, State) ->
+    Value = binary_to_list(iolist_to_binary(lists:reverse(Acc))),
+    State1 = [{Name, Value} | State],
+    fun (Next) -> parse_form_outer(Next, FileHandler, State1) end;
+parse_form_value({body, Data}, {Name, Acc}, FileHandler, State) ->
+    Acc1 = [Data | Acc],
+    fun (Next) -> parse_form_value(Next, {Name, Acc1}, FileHandler, State) end.
+
+parse_form_file(body_end, {Name, Handler}, FileHandler, State) ->
+    Value = Handler(eof),
+    State1 = [{Name, Value} | State],
+    fun (Next) -> parse_form_outer(Next, FileHandler, State1) end;
+parse_form_file({body, Data}, {Name, Handler}, FileHandler, State) ->
+    H1 = Handler(Data),
+    fun (Next) -> parse_form_file(Next, {Name, H1}, FileHandler, State) end.
+
+parse_multipart_request(Req, Callback) ->
+    %% TODO: Support chunked?
+    Length = list_to_integer(Req:get_header_value("content-length")),
+    Boundary = iolist_to_binary(
+		 get_boundary(Req:get_header_value("content-type"))),
+    Prefix = <<"\r\n--", Boundary/binary>>,
+    BS = size(Boundary),
+    Chunk = read_chunk(Req, Length),
+    Length1 = Length - size(Chunk),
+    <<"--", Boundary:BS/binary, "\r\n", Rest/binary>> = Chunk,
+    feed_mp(headers, #mp{boundary=Prefix,
+			 length=Length1,
+			 buffer=Rest,
+			 callback=Callback,
+			 req=Req}).
+
+parse_headers(<<>>) ->
+    [];
+parse_headers(Binary) ->
+    parse_headers(Binary, []).
+
+parse_headers(Binary, Acc) ->
+    case find_in_binary(<<"\r\n">>, Binary) of
+	{exact, N} ->
+	    <<Line:N/binary, "\r\n", Rest/binary>> = Binary,
+	    parse_headers(Rest, [split_header(Line) | Acc]);
+	not_found ->
+	    lists:reverse([split_header(Binary) | Acc])
+    end.
+
+split_header(Line) ->
+    {Name, [$: | Value]} = lists:splitwith(fun (C) -> C =/= $: end,
+					   binary_to_list(Line)),
+    {string:to_lower(string:strip(Name)),
+     mochiweb_util:parse_header(Value)}.
+
+read_chunk(Req, Length) when Length > 0 ->
+    case Length of
+	Length when Length < ?CHUNKSIZE ->
+	    Req:recv(Length);
+	_ ->
+	    Req:recv(?CHUNKSIZE)
+    end.
+
+read_more(State=#mp{length=Length, buffer=Buffer, req=Req}) ->
+    Data = read_chunk(Req, Length),
+    Buffer1 = <<Buffer/binary, Data/binary>>,
+    State#mp{length=Length - size(Data),
+	     buffer=Buffer1}.
+
+feed_mp(headers, State=#mp{buffer=Buffer, callback=Callback}) ->
+    {State1, P} = case find_in_binary(<<"\r\n\r\n">>, Buffer) of
+		      {exact, N} ->
+			  {State, N};
+		      _ ->
+			  S1 = read_more(State),
+			  %% Assume headers must be less than ?CHUNKSIZE
+			  {exact, N} = find_in_binary(<<"\r\n\r\n">>,
+						      S1#mp.buffer),
+			  {S1, N}
+		  end,
+    <<Headers:P/binary, "\r\n\r\n", Rest/binary>> = State1#mp.buffer,
+    NextCallback = Callback({headers, parse_headers(Headers)}),
+    feed_mp(body, State1#mp{buffer=Rest,
+			    callback=NextCallback});
+feed_mp(body, State=#mp{boundary=Prefix, buffer=Buffer, callback=Callback}) ->
+    case find_boundary(Prefix, Buffer) of
+	{end_boundary, Start, Skip} ->
+	    <<Data:Start/binary, _:Skip/binary, Rest/binary>> = Buffer,
+	    C1 = Callback({body, Data}),
+	    C2 = C1(body_end),
+	    {State#mp.length, Rest, C2(eof)};
+	{next_boundary, Start, Skip} ->
+	    <<Data:Start/binary, _:Skip/binary, Rest/binary>> = Buffer,
+	    C1 = Callback({body, Data}),
+	    feed_mp(headers, State#mp{callback=C1(body_end),
+				      buffer=Rest});
+	{maybe, Start} ->
+	    <<Data:Start/binary, Rest/binary>> = Buffer,
+	    feed_mp(body, read_more(State#mp{callback=Callback({body, Data}),
+					     buffer=Rest}));
+	not_found ->
+	    {Data, Rest} = {Buffer, <<>>},
+	    feed_mp(body, read_more(State#mp{callback=Callback({body, Data}),
+					     buffer=Rest}))
+    end.
+
+get_boundary(ContentType) ->
+    {"multipart/form-data", Opts} = mochiweb_util:parse_header(ContentType),
+    case proplists:get_value("boundary", Opts) of
+	S when is_list(S) ->
+	    S
+    end.
+
+find_in_binary(B, Data) when size(B) > 0 ->
+    case size(Data) - size(B) of
+	Last when Last < 0 ->
+	    partial_find(B, Data, 0, size(Data));
+	Last ->
+	    find_in_binary(B, size(B), Data, 0, Last)
+    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) ->
+    <<B1:K/binary, _/binary>> = B,
+    case D of
+	<<_Skip:N/binary, B1:K/binary>> ->
+	    {partial, N, K};
+	_ ->
+	    partial_find(B, D, 1 + N, K - 1)
+    end.
+
+find_boundary(Prefix, Data) ->
+    case find_in_binary(Prefix, Data) of
+	{exact, Skip} ->
+	    PrefixSkip = Skip + size(Prefix),
+	    case Data of
+		<<_:PrefixSkip/binary, "\r\n", _/binary>> ->
+		    {next_boundary, Skip, size(Prefix) + 2};
+		<<_:PrefixSkip/binary, "--\r\n", _/binary>> ->
+		    {end_boundary, Skip, size(Prefix) + 4};
+		_ when size(Data) < PrefixSkip + 4 ->
+		    %% Underflow
+		    {maybe, Skip};
+		_ ->
+		    %% False positive
+		    not_found
+	    end;
+	{partial, Skip, Length} when (Skip + Length) =:= size(Data) ->
+            %% Underflow
+            {maybe, Skip};
+        _ ->
+	    not_found
+    end.
+
+with_socket_server(ServerFun, ClientFun) ->
+    {ok, Server} = mochiweb_socket_server:start([{ip, "127.0.0.1"},
+						 {port, 0},
+						 {loop, ServerFun}]),
+    Port = mochiweb_socket_server:get(Server, port),
+    {ok, Client} = gen_tcp:connect("127.0.0.1", Port,
+				   [binary, {active, false}]),
+    Res = (catch ClientFun(Client)),
+    mochiweb_socket_server:stop(Server),
+    Res.
+
+fake_request(Socket, ContentType, Length) ->
+    mochiweb_request:new(Socket,
+			 'POST',
+			 "/multipart",
+			 {1,1},
+			 mochiweb_headers:make(
+			   [{"content-type", ContentType},
+			    {"content-length", Length}])).
+
+test_callback(Expect, [Expect | Rest]) ->
+    case Rest of
+	[] ->
+	    ok;
+	_ ->
+	    fun (Next) -> test_callback(Next, Rest) end
+    end.
+
+test_parse3() ->
+    ContentType = "multipart/form-data; boundary=---------------------------7386909285754635891697677882",
+    BinContent = <<"-----------------------------7386909285754635891697677882\r\nContent-Disposition: form-data; name=\"hidden\"\r\n\r\nmultipart message\r\n-----------------------------7386909285754635891697677882\r\nContent-Disposition: form-data; name=\"file\"; filename=\"test_file.txt\"\r\nContent-Type: text/plain\r\n\r\nWoo multiline text file\n\nLa la la\r\n-----------------------------7386909285754635891697677882--\r\n">>,
+    Expect = [{headers,
+	       [{"content-disposition",
+		 {"form-data", [{"name", "hidden"}]}}]},
+	      {body, <<"multipart message">>},
+	      body_end,
+	      {headers,
+	       [{"content-disposition",
+		 {"form-data", [{"name", "file"}, {"filename", "test_file.txt"}]}},
+		{"content-type", {"text/plain", []}}]},
+	      {body, <<"Woo multiline text file\n\nLa la la">>},
+	      body_end,
+	      eof],
+    TestCallback = fun (Next) -> test_callback(Next, Expect) end,
+    ServerFun = fun (Socket) ->
+			case gen_tcp:send(Socket, BinContent) of
+			    ok ->
+				exit(normal)
+			end
+		end,
+    ClientFun = fun (Socket) ->
+			Req = fake_request(Socket, ContentType,
+					   size(BinContent)),
+			Res = parse_multipart_request(Req, TestCallback),
+			{0, <<>>, ok} = Res,
+			ok
+		end,
+    ok = with_socket_server(ServerFun, ClientFun),
+    ok.
+
+
+test_parse2() ->
+    ContentType = "multipart/form-data; boundary=---------------------------6072231407570234361599764024",
+    BinContent = <<"-----------------------------6072231407570234361599764024\r\nContent-Disposition: form-data; name=\"hidden\"\r\n\r\nmultipart message\r\n-----------------------------6072231407570234361599764024\r\nContent-Disposition: form-data; name=\"file\"; filename=\"\"\r\nContent-Type: application/octet-stream\r\n\r\n\r\n-----------------------------6072231407570234361599764024--\r\n">>,
+    Expect = [{headers,
+	       [{"content-disposition",
+		 {"form-data", [{"name", "hidden"}]}}]},
+	      {body, <<"multipart message">>},
+	      body_end,
+	      {headers,
+	       [{"content-disposition",
+		 {"form-data", [{"name", "file"}, {"filename", ""}]}},
+		{"content-type", {"application/octet-stream", []}}]},
+	      {body, <<>>},
+	      body_end,
+	      eof],
+    TestCallback = fun (Next) -> test_callback(Next, Expect) end,
+    ServerFun = fun (Socket) ->
+			case gen_tcp:send(Socket, BinContent) of
+			    ok ->
+				exit(normal)
+			end
+		end,
+    ClientFun = fun (Socket) ->
+			Req = fake_request(Socket, ContentType,
+					   size(BinContent)),
+			Res = parse_multipart_request(Req, TestCallback),
+			{0, <<>>, ok} = Res,
+			ok
+		end,
+    ok = with_socket_server(ServerFun, ClientFun),
+    ok.
+
+handler_test(Filename, ContentType) ->
+    fun (Next) ->
+            handler_test_read(Next, {Filename, ContentType}, [])
+    end.
+
+handler_test_read(eof, {Filename, ContentType}, Acc) ->
+    Value = iolist_to_binary(lists:reverse(Acc)),
+    {Filename, ContentType, Value};
+handler_test_read(Data, H, Acc) ->
+    Acc1 = [Data | Acc],
+    fun (Next) -> handler_test_read(Next, H, Acc1) end.
+
+
+test_parse_form() ->
+    ContentType = "multipart/form-data; boundary=AaB03x",
+    "AaB03x" = get_boundary(ContentType),
+    Content = mochiweb_util:join(
+		["--AaB03x",
+		 "Content-Disposition: form-data; name=\"submit-name\"",
+		 "",
+		 "Larry",
+		 "--AaB03x",
+		 "Content-Disposition: form-data; name=\"files\";"
+		 ++ "filename=\"file1.txt\"",
+		 "Content-Type: text/plain",
+		 "",
+		 "... contents of file1.txt ...",
+		 "--AaB03x--",
+		 ""], "\r\n"),
+    BinContent = iolist_to_binary(Content),
+    ServerFun = fun (Socket) ->
+			case gen_tcp:send(Socket, BinContent) of
+			    ok ->
+				exit(normal)
+			end
+		end,
+    ClientFun = fun (Socket) ->
+			Req = fake_request(Socket, ContentType,
+					   size(BinContent)),
+			Res = parse_form(Req, fun handler_test/2),
+                        [{"submit-name", "Larry"},
+                         {"files", {"file1.txt", {"text/plain",[]},
+                                    <<"... contents of file1.txt ...">>}
+                         }] = Res,
+			ok
+		end,
+    ok = with_socket_server(ServerFun, ClientFun),
+    ok.
+
+test_parse() ->
+    ContentType = "multipart/form-data; boundary=AaB03x",
+    "AaB03x" = get_boundary(ContentType),
+    Content = mochiweb_util:join(
+		["--AaB03x",
+		 "Content-Disposition: form-data; name=\"submit-name\"",
+		 "",
+		 "Larry",
+		 "--AaB03x",
+		 "Content-Disposition: form-data; name=\"files\";"
+		 ++ "filename=\"file1.txt\"",
+		 "Content-Type: text/plain",
+		 "",
+		 "... contents of file1.txt ...",
+		 "--AaB03x--",
+		 ""], "\r\n"),
+    BinContent = iolist_to_binary(Content),
+    Expect = [{headers,
+	       [{"content-disposition",
+		 {"form-data", [{"name", "submit-name"}]}}]},
+	      {body, <<"Larry">>},
+	      body_end,
+	      {headers,
+	       [{"content-disposition",
+		 {"form-data", [{"name", "files"}, {"filename", "file1.txt"}]}},
+		 {"content-type", {"text/plain", []}}]},
+	      {body, <<"... contents of file1.txt ...">>},
+	      body_end,
+	      eof],
+    TestCallback = fun (Next) -> test_callback(Next, Expect) end,
+    ServerFun = fun (Socket) ->
+			case gen_tcp:send(Socket, BinContent) of
+			    ok ->
+				exit(normal)
+			end
+		end,
+    ClientFun = fun (Socket) ->
+			Req = fake_request(Socket, ContentType,
+					   size(BinContent)),
+			Res = parse_multipart_request(Req, TestCallback),
+			{0, <<>>, ok} = Res,
+			ok
+		end,
+    ok = with_socket_server(ServerFun, ClientFun),
+    ok.
+
+test_find_boundary() ->
+    B = <<"\r\n--X">>,
+    {next_boundary, 0, 7} = find_boundary(B, <<"\r\n--X\r\nRest">>),
+    {next_boundary, 1, 7} = find_boundary(B, <<"!\r\n--X\r\nRest">>),
+    {end_boundary, 0, 9} = find_boundary(B, <<"\r\n--X--\r\nRest">>),
+    {end_boundary, 1, 9} = find_boundary(B, <<"!\r\n--X--\r\nRest">>),
+    not_found = find_boundary(B, <<"--X\r\nRest">>),
+    {maybe, 0} = find_boundary(B, <<"\r\n--X\r">>),
+    {maybe, 1} = find_boundary(B, <<"!\r\n--X\r">>),
+    P = <<"\r\n-----------------------------16037454351082272548568224146">>,
+    B0 = <<55,212,131,77,206,23,216,198,35,87,252,118,252,8,25,211,132,229,
+          182,42,29,188,62,175,247,243,4,4,0,59, 13,10,45,45,45,45,45,45,45,
+          45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,
+          49,54,48,51,55,52,53,52,51,53,49>>,
+    {maybe, 30} = find_boundary(P, B0),
+    ok.
+
+test_find_in_binary() ->
+    {exact, 0} = find_in_binary(<<"foo">>, <<"foobarbaz">>),
+    {exact, 1} = find_in_binary(<<"oo">>, <<"foobarbaz">>),
+    {exact, 8} = find_in_binary(<<"z">>, <<"foobarbaz">>),
+    not_found = find_in_binary(<<"q">>, <<"foobarbaz">>),
+    {partial, 7, 2} = find_in_binary(<<"azul">>, <<"foobarbaz">>),
+    {exact, 0} = find_in_binary(<<"foobarbaz">>, <<"foobarbaz">>),
+    {partial, 0, 3} = find_in_binary(<<"foobar">>, <<"foo">>),
+    {partial, 1, 3} = find_in_binary(<<"foobar">>, <<"afoo">>),
+    ok.
+
+test() ->
+    test_find_in_binary(),
+    test_find_boundary(),
+    test_parse(),
+    test_parse2(),
+    test_parse3(),
+    test_parse_form(),
+    ok.

Added: incubator/couchdb/vendor/mochiweb/current/src/mochiweb_request.erl
URL: http://svn.apache.org/viewvc/incubator/couchdb/vendor/mochiweb/current/src/mochiweb_request.erl?rev=642953&view=auto
==============================================================================
--- incubator/couchdb/vendor/mochiweb/current/src/mochiweb_request.erl (added)
+++ incubator/couchdb/vendor/mochiweb/current/src/mochiweb_request.erl Mon Mar 31 03:30:27 2008
@@ -0,0 +1,700 @@
+%% @author Bob Ippolito <bob@mochimedia.com>
+%% @copyright 2007 Mochi Media, Inc.
+
+%% @doc MochiWeb HTTP Request abstraction.
+
+-module(mochiweb_request, [Socket, Method, RawPath, Version, Headers]).
+-author('bob@mochimedia.com').
+
+-include_lib("kernel/include/file.hrl").
+
+-define(QUIP, "Any of you quaids got a smint?").
+-define(READ_SIZE, 8192).
+
+-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]).
+-export([start_response/1, start_response_length/1, start_raw_response/1]).
+-export([respond/1, ok/1]).
+-export([not_found/0]).
+-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]).
+-export([test/0]).
+
+-define(SAVE_QS, mochiweb_request_qs).
+-define(SAVE_PATH, mochiweb_request_path).
+-define(SAVE_RECV, mochiweb_request_recv).
+-define(SAVE_BODY, mochiweb_request_body).
+-define(SAVE_BODY_LENGTH, mochiweb_request_body_length).
+-define(SAVE_POST, mochiweb_request_post).
+-define(SAVE_COOKIE, mochiweb_request_cookie).
+
+%% @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 response(). A mochiweb_response parameterized module instance.
+%% @type ioheaders() = headers() | [{key(), value()}].
+
+% 10 second default idle timeout
+-define(IDLE_TIMEOUT, 10000).
+
+% Maximum recv_body() length of 1MB
+-define(MAX_RECV_BODY, (1024*1024)).
+
+%% @spec get_header_value(K) -> undefined | Value
+%% @doc Get the value of a given request header.
+get_header_value(K) ->
+    mochiweb_headers:get_value(K, Headers).
+
+get_primary_header_value(K) ->
+    mochiweb_headers:get_primary_value(K, Headers).
+
+%% @type field() = socket | method | raw_path | version | headers | peer | path | body_length | range
+
+%% @spec get(field()) -> term()
+%% @doc Return the internal representation of the given field.
+get(socket) ->
+    Socket;
+get(method) ->
+    Method;
+get(raw_path) ->
+    RawPath;
+get(version) ->
+    Version;
+get(headers) ->
+    Headers;
+get(peer) ->
+    case inet:peername(Socket) of
+	{ok, {Addr={10, _, _, _}, _Port}} ->
+	    case get_header_value("x-forwarded-for") 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
+		undefined ->
+		    "127.0.0.1";
+		Hosts ->
+		    string:strip(lists:last(string:tokens(Hosts, ",")))
+	    end;
+	{ok, {Addr, _Port}} ->
+	    inet_parse:ntoa(Addr)
+    end;
+get(path) ->
+    case erlang:get(?SAVE_PATH) of
+	undefined ->
+	    {Path0, _, _} = mochiweb_util:urlsplit_path(RawPath),
+            Path = mochiweb_util:unquote(Path0),
+	    put(?SAVE_PATH, Path),
+	    Path;
+	Cached ->
+	    Cached
+    end;
+get(body_length) ->
+    erlang:get(?SAVE_BODY_LENGTH);
+get(range) ->
+    case get_header_value(range) of
+        undefined ->
+            undefined;
+        RawRange ->
+            parse_range_request(RawRange)
+    end.
+
+%% @spec dump() -> {mochiweb_request, [{atom(), term()}]}
+%% @doc Dump the internal representation to a "human readable" set of terms
+%%      for debugging/inspection purposes.
+dump() ->
+    {?MODULE, [{method, Method},
+	       {version, Version},
+	       {raw_path, RawPath},
+	       {headers, mochiweb_headers:to_list(Headers)}]}.
+
+%% @spec send(iodata()) -> ok
+%% @doc Send data over the socket.
+send(Data) ->
+    case gen_tcp:send(Socket, Data) of
+	ok ->
+	    ok;
+	_ ->
+	    exit(normal)
+    end.
+
+%% @spec recv(integer()) -> binary()
+%% @doc Receive Length bytes from the client as a binary, with the default
+%%      idle timeout.
+recv(Length) ->
+    recv(Length, ?IDLE_TIMEOUT).
+
+%% @spec recv(integer(), integer()) -> binary()
+%% @doc Receive Length bytes from the client as a binary, with the given
+%%      Timeout in msec.
+recv(Length, Timeout) ->
+    case gen_tcp:recv(Socket, Length, Timeout) of
+	{ok, Data} ->
+	    put(?SAVE_RECV, true),
+	    Data;
+	_ ->
+	    exit(normal)
+    end.
+
+%% @spec body_length() -> 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
+        undefined ->
+            case get_header_value("content-length") of
+                undefined ->
+                    undefined;
+                Length ->
+                    list_to_integer(Length)
+            end;
+        "chunked" ->
+            chunked;
+        Unknown ->
+            {unknown_transfer_encoding, Unknown}
+    end.
+
+
+%% @spec recv_body() -> 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).
+
+%% @spec recv_body(integer()) -> binary()
+%% @doc Receive the body of the HTTP request (defined by Content-Length).
+%%      Will receive up to MaxBody bytes.
+recv_body(MaxBody) ->
+    case get_header_value("expect") of
+        "100-continue" ->
+            start_raw_response({100, gb_trees:empty()});
+        _Else ->
+            ok
+    end,
+    Body = case body_length() of
+               undefined ->
+                   undefined;
+               {unknown_transfer_encoding, Unknown} ->
+                   exit({unknown_transfer_encoding, Unknown});
+               chunked ->
+                   read_chunked_body(MaxBody, []);
+               0 ->
+                   <<>>;
+               Length when is_integer(Length), Length =< MaxBody ->
+                   recv(Length);
+               Length ->
+                   exit({body_too_large, Length})
+           end,
+    put(?SAVE_BODY, Body),
+    Body.
+
+
+%% @spec start_response({integer(), ioheaders()}) -> 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}) ->
+    HResponse = mochiweb_headers:make(ResponseHeaders),
+    HResponse1 = mochiweb_headers:default_from_list(server_headers(),
+						    HResponse),
+    start_raw_response({Code, HResponse1}).
+
+%% @spec start_raw_response({integer(), headers()}) -> response()
+%% @doc Start the HTTP response by sending the Code HTTP response and
+%%      ResponseHeaders.
+start_raw_response({Code, ResponseHeaders}) ->
+    F = fun ({K, V}, Acc) ->
+		[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]),
+    mochiweb:new_response({THIS, Code, ResponseHeaders}).
+
+
+%% @spec start_response_length({integer(), ioheaders(), integer()}) -> 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}) ->
+    HResponse = mochiweb_headers:make(ResponseHeaders),
+    HResponse1 = mochiweb_headers:enter("Content-Length", Length, HResponse),
+    start_response({Code, HResponse1}).
+
+%% @spec respond({integer(), ioheaders(), iodata() | chunked | {file, IoDevice}}) -> 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}}) ->
+    Length = iodevice_size(IoDevice),
+    Response = start_response_length({Code, ResponseHeaders, Length}),
+    case Method of
+        'HEAD' ->
+            ok;
+        _ ->
+            iodevice_stream(IoDevice)
+    end,
+    Response;
+respond({Code, ResponseHeaders, chunked}) ->
+    HResponse = mochiweb_headers:make(ResponseHeaders),
+    HResponse1 = case Method of
+		     'HEAD' ->
+			 %% This is what Google does, http://www.google.com/
+			 %% is chunked but HEAD gets Content-Length: 0.
+			 %% The RFC is ambiguous so emulating Google is smart.
+			 mochiweb_headers:enter("Content-Length", "0",
+						HResponse);
+		     _ ->
+			 mochiweb_headers:enter("Transfer-Encoding", "chunked",
+						HResponse)
+		 end,
+    start_response({Code, HResponse1});
+respond({Code, ResponseHeaders, Body}) ->
+    Response = start_response_length({Code, ResponseHeaders, iolist_size(Body)}),
+    case Method of
+	'HEAD' ->
+	    ok;
+	_ ->
+	    send(Body)
+    end,
+    Response.
+
+%% @spec not_found() -> response()
+%% @doc respond({404, [{"Content-Type", "text/plain"}], "Not found."}).
+not_found() ->
+    respond({404, [{"Content-Type", "text/plain"}], <<"Not found.">>}).
+
+%% @spec ok({value(), iodata()} | {value(), ioheaders(), iodata() | {file, IoDevice}}) ->
+%%           response()
+%% @doc respond({200, [{"Content-Type", ContentType} | Headers], Body}).
+ok({ContentType, Body}) ->
+    ok({ContentType, [], Body});
+ok({ContentType, ResponseHeaders, Body}) ->
+    HResponse = mochiweb_headers:make(ResponseHeaders),
+    case THIS:get(range) of
+        X when X =:= undefined; X =:= fail ->
+            HResponse1 = mochiweb_headers:enter("Content-Type", ContentType, HResponse),
+            respond({200, HResponse1, Body});
+        Ranges ->
+            {PartList, Size} = range_parts(Body, Ranges),
+            case PartList of
+                [] -> %% no valid ranges
+                    HResponse1 = mochiweb_headers:enter("Content-Type",
+                                                        ContentType,
+                                                        HResponse),
+                    %% could be 416, for now we'll just return 200
+                    respond({200, HResponse1, Body});
+                PartList ->
+                    {RangeHeaders, RangeBody} =
+                        parts_to_body(PartList, ContentType, Size),
+                    HResponse1 = mochiweb_headers:enter_from_list(
+                                   [{"Accept-Ranges", "bytes"} |
+                                    RangeHeaders],
+                                   HResponse),
+                    respond({206, HResponse1, RangeBody})
+            end
+    end.
+
+%% @spec should_close() -> bool()
+%% @doc Return true if the connection must be closed. If false, using
+%%      Keep-Alive should be safe.
+should_close() ->
+    DidNotRecv = erlang:get(mochiweb_request_recv) =:= undefined,
+    Version < {1, 0}
+        % Connection: close
+	orelse get_header_value("connection") =:= "close"
+        % HTTP 1.0 requires Connection: Keep-Alive
+	orelse (Version =:= {1, 0}
+		andalso get_header_value("connection") /= "Keep-Alive")
+        % unread data left on the socket, can't safely continue
+	orelse (DidNotRecv
+		andalso get_header_value("content-length") /= undefined).
+
+%% @spec cleanup() -> 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_POST,
+		       ?SAVE_COOKIE]],
+    ok.
+
+%% @spec parse_qs() -> [{Key::string(), Value::string()}]
+%% @doc Parse the query string of the URL.
+parse_qs() ->
+    case erlang:get(?SAVE_QS) of
+	undefined ->
+	    {_, QueryString, _} = mochiweb_util:urlsplit_path(RawPath),
+	    Parsed = mochiweb_util:parse_qs(QueryString),
+	    put(?SAVE_QS, Parsed),
+	    Parsed;
+	Cached ->
+	    Cached
+    end.
+
+%% @spec get_cookie_value(Key::string) -> string() | undefined
+%% @doc Get the value of the given cookie.
+get_cookie_value(Key) ->
+    proplists:get_value(Key, parse_cookie()).
+
+%% @spec parse_cookie() -> [{Key::string(), Value::string()}]
+%% @doc Parse the cookie header.
+parse_cookie() ->
+    case erlang:get(?SAVE_COOKIE) of
+	undefined ->
+	    Cookies = case get_header_value("cookie") of
+			  undefined ->
+			      [];
+			  Value ->
+			      mochiweb_cookies:parse_cookie(Value)
+		      end,
+	    put(?SAVE_COOKIE, Cookies),
+	    Cookies;
+	Cached ->
+	    Cached
+    end.
+
+%% @spec parse_post() -> [{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() ->
+    case erlang:get(?SAVE_POST) of
+	undefined ->
+	    Parsed = case recv_body() of
+			 undefined ->
+			     [];
+			 Binary ->
+			     case get_primary_header_value("content-type") of
+				 "application/x-www-form-urlencoded" ->
+				     mochiweb_util:parse_qs(Binary);
+				 _ ->
+				     []
+			     end
+		     end,
+	    put(?SAVE_POST, Parsed),
+	    Parsed;
+	Cached ->
+	    Cached
+    end.
+
+read_chunked_body(Max, Acc) ->
+    case read_chunk_length() of
+	0 ->
+	    read_chunk(0),
+	    iolist_to_binary(lists:reverse(Acc));
+	Length when Length > Max ->
+	    exit({body_too_large, chunked});
+	Length ->
+	    read_chunked_body(Max - Length, [read_chunk(Length) | Acc])
+    end.
+
+%% @spec read_chunk_length() -> integer()
+%% @doc Read the length of the next HTTP chunk.
+read_chunk_length() ->
+    inet:setopts(Socket, [{packet, line}]),
+    case gen_tcp:recv(Socket, 0, ?IDLE_TIMEOUT) of
+        {ok, Header} ->
+            inet:setopts(Socket, [{packet, raw}]),
+            Splitter = fun (C) ->
+                               C =/= $\r andalso C =/= $\n andalso C =/= $
+                       end,
+            {Hex, _Rest} = lists:splitwith(Splitter, binary_to_list(Header)),
+            mochihex:to_int(Hex);
+        _ ->
+            exit(normal)
+    end.
+
+%% @spec read_chunk(integer()) -> 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) ->
+    inet:setopts(Socket, [{packet, line}]),
+    F = fun (F1, Acc) ->
+                case gen_tcp:recv(Socket, 0, ?IDLE_TIMEOUT) of
+		    {ok, <<"\r\n">>} ->
+			Acc;
+		    {ok, Footer} ->
+			F1(F1, [Footer | Acc]);
+                    _ ->
+                        exit(normal)
+		end
+	end,
+    Footers = F(F, []),
+    inet:setopts(Socket, [{packet, raw}]),
+    Footers;
+read_chunk(Length) ->
+    case gen_tcp:recv(Socket, 2 + Length, ?IDLE_TIMEOUT) of
+        {ok, <<Chunk:Length/binary, "\r\n">>} ->
+            Chunk;
+        _ ->
+            exit(normal)
+    end.
+
+%% @spec serve_file(Path, DocRoot) -> Response
+%% @doc Serve a file relative to DocRoot.
+serve_file(Path, DocRoot) ->
+    FullPath = filename:join([DocRoot, Path]),
+    File = case filelib:is_dir(FullPath) of
+	       true ->
+		   filename:join([FullPath, "index.html"]);
+	       false ->
+		   FullPath
+	   end,
+    case lists:prefix(DocRoot, File) of
+	true ->
+	    case file:open(File, [raw, binary]) of
+		{ok, IoDevice} ->
+		    ContentType = mochiweb_util:guess_mime(File),
+		    Res = ok({ContentType, {file, IoDevice}}),
+                    file:close(IoDevice),
+                    Res;
+		_ ->
+		    not_found()
+	    end;
+	false ->
+	    not_found()
+    end.
+
+
+%% Internal API
+
+server_headers() ->
+    [{"Server", "MochiWeb/1.0 (" ++ ?QUIP ++ ")"},
+     {"Date", httpd_util:rfc1123_date()}].
+
+make_io(Atom) when is_atom(Atom) ->
+    atom_to_list(Atom);
+make_io(Integer) when is_integer(Integer) ->
+    integer_to_list(Integer);
+make_io(Io) when is_list(Io); is_binary(Io) ->
+    Io.
+
+make_code(X) when is_integer(X) ->
+    [integer_to_list(X), [" " | httpd_util:reason_phrase(X)]];
+make_code(Io) when is_list(Io); is_binary(Io) ->
+    Io.
+
+make_version({1, 0}) ->
+    <<"HTTP/1.0 ">>;
+make_version(_) ->
+    <<"HTTP/1.1 ">>.
+
+iodevice_stream(IoDevice) ->
+    case file:read(IoDevice, ?READ_SIZE) of
+        eof ->
+            ok;
+        {ok, Data} ->
+            ok = send(Data),
+            iodevice_stream(IoDevice)
+    end.
+
+
+parts_to_body([{Start, End, Body}], ContentType, Size) ->
+    %% return body for a range reponse with a single body
+    HeaderList = [{"Content-Type", ContentType},
+                  {"Content-Range",
+                   ["bytes ",
+                    make_io(Start), "-", make_io(End),
+                    "/", make_io(Size)]}],
+    {HeaderList, Body};
+parts_to_body(BodyList, ContentType, Size) when is_list(BodyList) ->
+    %% return
+    %% header Content-Type: multipart/byteranges; boundary=441934886133bdee4
+    %% and multipart body
+    Boundary = mochihex:to_hex(crypto:rand_bytes(8)),
+    HeaderList = [{"Content-Type",
+                   ["multipart/byteranges; ",
+                    "boundary=", Boundary]}],
+    MultiPartBody = multipart_body(BodyList, ContentType, Boundary, Size),
+
+    {HeaderList, MultiPartBody}.
+
+multipart_body([], _ContentType, Boundary, _Size) ->
+    ["--", Boundary, "--\r\n"];
+multipart_body([{Start, End, Body} | BodyList], ContentType, Boundary, Size) ->
+    ["--", Boundary, "\r\n",
+     "Content-Type: ", ContentType, "\r\n",
+     "Content-Range: ",
+         "bytes ", make_io(Start), "-", make_io(End),
+             "/", make_io(Size), "\r\n\r\n",
+     Body, "\r\n"
+     | multipart_body(BodyList, ContentType, Boundary, Size)].
+
+iodevice_size(IoDevice) ->
+    {ok, Size} = file:position(IoDevice, eof),
+    {ok, 0} = file:position(IoDevice, bof),
+    Size.
+
+range_parts({file, IoDevice}, Ranges) ->
+    Size = iodevice_size(IoDevice),
+    F = fun (Spec, Acc) ->
+                case range_skip_length(Spec, Size) of
+                    invalid_range ->
+                        Acc;
+                    V ->
+                        [V | Acc]
+                end
+        end,
+    LocNums = lists:foldr(F, [], Ranges),
+    {ok, Data} = file:pread(IoDevice, LocNums),
+    Bodies = lists:zipwith(fun ({Skip, Length}, PartialBody) ->
+                                   {Skip, Skip + Length - 1, PartialBody}
+                           end,
+                           LocNums, Data),
+    {Bodies, Size};
+
+range_parts(Body0, Ranges) ->
+    Body = iolist_to_binary(Body0),
+    Size = size(Body),
+    F = fun(Spec, Acc) ->
+                case range_skip_length(Spec, Size) of
+                    invalid_range ->
+                        Acc;
+                    {Skip, Length} ->
+                        <<_:Skip/binary, PartialBody:Length/binary, _/binary>> = Body,
+                        [{Skip, Skip + Length - 1, PartialBody} | Acc]
+                end
+        end,
+    {lists:foldr(F, [], Ranges), Size}.
+
+range_skip_length(Spec, Size) ->
+    case Spec of
+        {none, R} when R =< Size, R >= 0 ->
+            {Size - R, R};
+        {none, _OutOfRange} ->
+            {0, Size};
+        {R, none} when R >= 0, R < Size ->
+            {R, Size - R};
+        {_OutOfRange, none} ->
+            invalid_range;
+        {Start, End} when 0 =< Start, Start =< End, End < Size ->
+            {Start, End - Start + 1};
+        {_OutOfRange, _End} ->
+            invalid_range
+    end.
+
+parse_range_request(RawRange) when is_list(RawRange) ->
+    try
+        "bytes=" ++ RangeString = RawRange,
+        Ranges = string:tokens(RangeString, ","),
+        lists:map(fun ("-" ++ V)  ->
+                          {none, list_to_integer(V)};
+                      (R) ->
+                          case string:tokens(R, "-") of
+                              [S1, S2] ->
+                                  {list_to_integer(S1), list_to_integer(S2)};
+                              [S] ->
+                                  {list_to_integer(S), none}
+                          end
+                  end,
+                  Ranges)
+    catch
+        _:_ ->
+            fail
+    end.
+
+
+test() ->
+    ok = test_range(),
+    ok.
+
+test_range() ->
+    %% valid, single ranges
+    io:format("Testing parse_range_request with valid single ranges~n"),
+    io:format("1"),
+    [{20, 30}] = parse_range_request("bytes=20-30"),
+    io:format("2"),
+    [{20, none}] = parse_range_request("bytes=20-"),
+    io:format("3"),
+    [{none, 20}] = parse_range_request("bytes=-20"),
+    io:format(".. ok ~n"),
+
+
+    %% invalid, single ranges
+    io:format("Testing parse_range_request with invalid ranges~n"),
+    io:format("1"),
+    fail = parse_range_request(""),
+    io:format("2"),
+    fail = parse_range_request("garbage"),
+    io:format("3"),
+    fail = parse_range_request("bytes=-20-30"),
+    io:format(".. ok ~n"),
+
+    %% valid, multiple range
+    io:format("Testing parse_range_request with valid multiple ranges~n"),
+    io:format("1"),
+    [{20, 30}, {50, 100}, {110, 200}] = 
+        parse_range_request("bytes=20-30,50-100,110-200"),
+    io:format("2"),
+    [{20, none}, {50, 100}, {none, 200}] = 
+        parse_range_request("bytes=20-,50-100,-200"),
+    io:format(".. ok~n"),
+    
+    %% no ranges
+    io:format("Testing out parse_range_request with no ranges~n"),
+    io:format("1"),
+    [] = parse_range_request("bytes="),
+    io:format(".. ok~n"),
+    
+    Body = <<"012345678901234567890123456789012345678901234567890123456789">>,
+    BodySize = size(Body), %% 60
+    BodySize = 60,
+
+    %% these values assume BodySize =:= 60
+    io:format("Testing out range_skip_length on valid ranges~n"),
+    io:format("1"),
+    {1,9} = range_skip_length({1,9}, BodySize), %% 1-9
+    io:format("2"),
+    {10,10} = range_skip_length({10,19}, BodySize), %% 10-19
+    io:format("3"),
+    {40, 20} = range_skip_length({none, 20}, BodySize), %% -20
+    io:format("4"),
+    {30, 30} = range_skip_length({30, none}, BodySize), %% 30-
+    io:format(".. ok ~n"),
+    
+    %% valid edge cases for range_skip_length
+    io:format("Testing out range_skip_length on valid edge case ranges~n"),
+    io:format("1"),
+    {BodySize, 0} = range_skip_length({none, 0}, BodySize),
+    io:format("2"),
+    {0, BodySize} = range_skip_length({none, BodySize}, BodySize),
+    io:format("3"),
+    {0, BodySize} = range_skip_length({0, none}, BodySize),
+    BodySizeLess1 = BodySize - 1,
+    io:format("4"),
+    {BodySizeLess1, 1} = range_skip_length({BodySize - 1, none}, BodySize),
+
+    %% out of range, return whole thing
+    io:format("5"),
+    {0, BodySize} = range_skip_length({none, BodySize + 1}, BodySize),
+    io:format("6"),
+    {0, BodySize} = range_skip_length({none, -1}, BodySize),
+    io:format(".. ok ~n"),
+
+    %% invalid ranges
+    io:format("Testing out range_skip_length on invalid ranges~n"),
+    io:format("1"),
+    invalid_range = range_skip_length({-1, 30}, BodySize),
+    io:format("2"),
+    invalid_range = range_skip_length({0, BodySize + 1}, BodySize),
+    io:format("3"),
+    invalid_range = range_skip_length({-1, BodySize + 1}, BodySize),
+    io:format("4"),
+    invalid_range = range_skip_length({BodySize, 40}, BodySize),
+    io:format("5"),
+    invalid_range = range_skip_length({-1, none}, BodySize),
+    io:format("6"),
+    invalid_range = range_skip_length({BodySize, none}, BodySize),
+    io:format(".. ok ~n"),
+    ok.
+    



Mime
View raw message