%% @author Bob Ippolito <bob@mochimedia.com>
%% @copyright 2007 Mochi Media, Inc.
%%
%% Permission is hereby granted, free of charge, to any person obtaining a
%% copy of this software and associated documentation files (the "Software"),
%% to deal in the Software without restriction, including without limitation
%% the rights to use, copy, modify, merge, publish, distribute, sublicense,
%% and/or sell copies of the Software, and to permit persons to whom the
%% Software is furnished to do so, subject to the following conditions:
%%
%% The above copyright notice and this permission notice shall be included in
%% all copies or substantial portions of the Software.
%%
%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
%% DEALINGS IN THE SOFTWARE.

%% @doc Utilities for parsing multipart/form-data.

-module(mochiweb_multipart).

-author('bob@mochimedia.com').

-export([parse_form/1, parse_form/2]).

-export([parse_multipart_request/2]).

-export([parts_to_body/3, parts_to_multipart_body/4]).

-export([default_file_handler/2]).

-define(CHUNKSIZE, 4096).

-record(mp,
	{state, boundary, length, buffer, callback, req}).

%% TODO: DOCUMENT THIS MODULE.
%% @type key() = atom() | string() | binary().
%% @type value() = atom() | iolist() | integer().
%% @type header() = {key(), value()}.
%% @type bodypart() = {Start::integer(), End::integer(), Body::iolist()}.
%% @type formfile() = {Name::string(), ContentType::string(), Content::binary()}.
%% @type request().
%% @type file_handler() = (Filename::string(), ContentType::string()) -> file_handler_callback().
%% @type file_handler_callback() = (binary() | eof) -> file_handler_callback() | term().

%% @spec parts_to_body([bodypart()], ContentType::string(),
%%                     Size::integer()) -> {[header()], iolist()}
%% @doc Return {[header()], iolist()} representing the body for the given
%%      parts, may be a single part or multipart.
parts_to_body([{Start, End, Body}], ContentType,
	      Size) ->
    HeaderList = [{"Content-Type", ContentType},
		  {"Content-Range",
		   ["bytes ", mochiweb_util:make_io(Start), "-",
		    mochiweb_util:make_io(End), "/",
		    mochiweb_util:make_io(Size)]}],
    {HeaderList, Body};
parts_to_body(BodyList, ContentType, Size)
    when is_list(BodyList) ->
    parts_to_multipart_body(BodyList, ContentType, Size,
			    mochihex:to_hex(crypto:strong_rand_bytes(8))).

%% @spec parts_to_multipart_body([bodypart()], ContentType::string(),
%%                               Size::integer(), Boundary::string()) ->
%%           {[header()], iolist()}
%% @doc Return {[header()], iolist()} representing the body for the given
%%      parts, always a multipart response.
parts_to_multipart_body(BodyList, ContentType, Size,
			Boundary) ->
    HeaderList = [{"Content-Type",
		   ["multipart/byteranges; ", "boundary=", Boundary]}],
    MultiPartBody = multipart_body(BodyList, ContentType,
				   Boundary, Size),
    {HeaderList, MultiPartBody}.

%% @spec multipart_body([bodypart()], ContentType::string(),
%%                      Boundary::string(), Size::integer()) -> iolist()
%% @doc Return the representation of a multipart body for the given [bodypart()].
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 ",
     mochiweb_util:make_io(Start), "-",
     mochiweb_util:make_io(End), "/",
     mochiweb_util:make_io(Size), "\r\n\r\n", Body, "\r\n"
     | multipart_body(BodyList, ContentType, Boundary,
		      Size)].

%% @spec parse_form(request()) -> [{string(), string() | formfile()}]
%% @doc Parse a multipart form from the given request using the in-memory
%%      default_file_handler/2.
parse_form(Req) ->
    parse_form(Req, fun default_file_handler/2).

%% @spec parse_form(request(), F::file_handler()) -> [{string(), string() | term()}]
%% @doc Parse a multipart form from the given request using the given file_handler().
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.

default_file_handler(Filename, ContentType) ->
    default_file_handler_1(Filename, ContentType, []).

default_file_handler_1(Filename, ContentType, Acc) ->
    fun (eof) ->
	    Value = iolist_to_binary(lists:reverse(Acc)),
	    {Filename, ContentType, Value};
	(Next) ->
	    default_file_handler_1(Filename, ContentType,
				   [Next | Acc])
    end.

parse_multipart_request({ReqM, _} = Req, Callback) ->
    %% TODO: Support chunked?
    Length =
	list_to_integer(ReqM:get_combined_header_value("content-length",
						       Req)),
    Boundary =
	iolist_to_binary(get_boundary(ReqM:get_header_value("content-type",
							    Req))),
    Prefix = <<"\r\n--", Boundary/binary>>,
    BS = byte_size(Boundary),
    Chunk = read_chunk(Req, Length),
    Length1 = Length - byte_size(Chunk),
    <<"--", Boundary:BS/binary, "\r\n", Rest/binary>> =
	Chunk,
    feed_mp(headers,
	    flash_multipart_hack(#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({ReqM, _} = Req, Length) when Length > 0 ->
    case Length of
      Length when Length < (?CHUNKSIZE) ->
	  ReqM:recv(Length, Req);
      _ -> ReqM:recv(?CHUNKSIZE, Req)
    end.

read_more(State = #mp{length = Length, buffer = Buffer,
		      req = Req}) ->
    Data = read_chunk(Req, Length),
    Buffer1 = <<Buffer/binary, Data/binary>>,
    flash_multipart_hack(State#mp{length =
				      Length - byte_size(Data),
				  buffer = Buffer1}).

flash_multipart_hack(State = #mp{length = 0,
				 buffer = Buffer, boundary = Prefix}) ->
    %% http://code.google.com/p/mochiweb/issues/detail?id=22
    %% Flash doesn't terminate multipart with \r\n properly so we fix it up here
    PrefixSize = size(Prefix),
    case size(Buffer) - (2 + PrefixSize) of
      Seek when Seek >= 0 ->
	  case Buffer of
	    <<_:Seek/binary, Prefix:PrefixSize/binary, "--">> ->
		Buffer1 = <<Buffer/binary, "\r\n">>,
		State#mp{buffer = Buffer1};
	    _ -> State
	  end;
      _ -> State
    end;
flash_multipart_hack(State) -> State.

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}) ->
    Boundary = find_boundary(Prefix, Buffer),
    case Boundary 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.

%% @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(P, Data, 0, DS);
      Last ->
	  case binary:match(Data, P) of
	    {Pos, _} -> {exact, Pos};
	    nomatch -> partial_find(P, Data, Last + 1, PS - 1)
	  end
    end.

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.

%%
%% Tests
%%
-ifdef(TEST).

-include_lib("eunit/include/eunit.hrl").

ssl_cert_opts() ->
    {ok, Cwd} = file:get_cwd(),
    CertDir = filename:join(
        case filename:basename(Cwd) of
            %% rebar2 compatibility
            ".eunit" -> [".."];
            _ -> []
        end ++ ["support", "test-materials"]),
    CertFile = filename:join(CertDir, "test_ssl_cert.pem"),
    KeyFile = filename:join(CertDir, "test_ssl_key.pem"),
    [{certfile, CertFile}, {keyfile, KeyFile}].

with_socket_server(Transport, ServerFun, ClientFun) ->
    ServerOpts0 = [{ip, "127.0.0.1"}, {port, 0},
		   {loop, ServerFun}],
    ServerOpts = case Transport of
		   plain -> ServerOpts0;
		   ssl ->
		       ServerOpts0 ++
			 [{ssl, true}, {ssl_opts, ssl_cert_opts()}]
		 end,
    {ok, Server} =
	mochiweb_socket_server:start_link(ServerOpts),
    Port = mochiweb_socket_server:get(Server, port),
    ClientOpts = [binary, {active, false}],
    {ok, Client} = case Transport of
		     plain -> gen_tcp:connect("127.0.0.1", Port, ClientOpts);
		     ssl ->
			 ClientOpts1 =
			     mochiweb_test_util:ssl_client_opts(ClientOpts),
			 {ok, SslSocket} = ssl:connect("127.0.0.1", Port,
						       ClientOpts1),
			 {ok, {ssl, SslSocket}}
		   end,
    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({body, <<>>}, Rest = [body_end | _]) ->
    %% When expecting the body_end we might get an empty binary
    fun (Next) -> test_callback(Next, Rest) end;
test_callback({body, Got}, [{body, Expect} | Rest])
    when Got =/= Expect ->
    %% Partial response
    GotSize = size(Got),
    <<Got:GotSize/binary, Expect1/binary>> = Expect,
    fun (Next) ->
	    test_callback(Next, [{body, Expect1} | Rest])
    end;
test_callback(Got, [Expect | Rest]) ->
    ?assertEqual(Got, Expect),
    case Rest of
      [] -> ok;
      _ -> fun (Next) -> test_callback(Next, Rest) end
    end.

parse3_http_test() -> parse3(plain).

parse3_https_test() -> parse3(ssl).

parse3(Transport) ->
    ContentType =
	"multipart/form-data; boundary=---------------"
	"------------7386909285754635891697677882",
    BinContent =
	<<"-----------------------------7386909285754635"
	  "891697677882\r\nContent-Disposition: "
	  "form-data; name=\"hidden\"\r\n\r\nmultipart "
	  "message\r\n-----------------------------73869"
	  "09285754635891697677882\r\nContent-Dispositio"
	  "n: form-data; name=\"file\"; filename=\"test_"
	  "file.txt\"\r\nContent-Type: text/plain\r\n\r\n"
	  "Woo multiline text file\n\nLa la la\r\n------"
	  "-----------------------7386909285754635891697"
	  "677882--\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, _Opts) ->
			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(Transport, ServerFun,
			    ClientFun),
    ok.

parse2_http_test() -> parse2(plain).

parse2_https_test() -> parse2(ssl).

parse2(Transport) ->
    ContentType =
	"multipart/form-data; boundary=---------------"
	"------------6072231407570234361599764024",
    BinContent =
	<<"-----------------------------6072231407570234"
	  "361599764024\r\nContent-Disposition: "
	  "form-data; name=\"hidden\"\r\n\r\nmultipart "
	  "message\r\n-----------------------------60722"
	  "31407570234361599764024\r\nContent-Dispositio"
	  "n: form-data; name=\"file\"; filename=\"\"\r\n"
	  "Content-Type: application/octet-stream\r\n\r\n\r\n"
	  "-----------------------------6072231407570234"
	  "361599764024--\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, _Opts) ->
			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(Transport, ServerFun,
			    ClientFun),
    ok.

parse_form_http_test() -> do_parse_form(plain).

parse_form_https_test() -> do_parse_form(ssl).

do_parse_form(Transport) ->
    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, _Opts) ->
			ok = mochiweb_socket:send(Socket, BinContent),
			exit(normal)
		end,
    ClientFun = fun (Socket) ->
			Req = fake_request(Socket, ContentType,
					   byte_size(BinContent)),
			Res = parse_form(Req),
			[{"submit-name", "Larry"},
			 {"files",
			  {"file1.txt", {"text/plain", []},
			   <<"... contents of file1.txt ...">>}}] =
			    Res,
			ok
		end,
    ok = with_socket_server(Transport, ServerFun,
			    ClientFun),
    ok.

parse_http_test() -> do_parse(plain).

parse_https_test() -> do_parse(ssl).

do_parse(Transport) ->
    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, _Opts) ->
			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(Transport, ServerFun,
			    ClientFun),
    ok.

parse_partial_body_boundary_http_test() ->
    parse_partial_body_boundary(plain).

parse_partial_body_boundary_https_test() ->
    parse_partial_body_boundary(ssl).

parse_partial_body_boundary(Transport) ->
    Boundary = string:copies("$", 2048),
    ContentType = "multipart/form-data; boundary=" ++
		    Boundary,
    ?assertEqual(Boundary, (get_boundary(ContentType))),
    Content = mochiweb_util:join(["--" ++ Boundary,
				  "Content-Disposition: form-data; name=\"submit"
				  "-name\"",
				  "", "Larry", "--" ++ Boundary,
				  "Content-Disposition: form-data; name=\"files\";"
				    ++ "filename=\"file1.txt\"",
				  "Content-Type: text/plain", "",
				  "... contents of file1.txt ...",
				  "--" ++ Boundary ++ "--", ""],
				 "\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, _Opts) ->
			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(Transport, ServerFun,
			    ClientFun),
    ok.

parse_large_header_http_test() ->
    parse_large_header(plain).

parse_large_header_https_test() ->
    parse_large_header(ssl).

parse_large_header(Transport) ->
    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",
				  "x-large-header: " ++
				    string:copies("%", 4096),
				  "", "... 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", []}},
		{"x-large-header", {string:copies("%", 4096), []}}]},
	      {body, <<"... contents of file1.txt ...">>}, body_end,
	      eof],
    TestCallback = fun (Next) -> test_callback(Next, Expect)
		   end,
    ServerFun = fun (Socket, _Opts) ->
			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(Transport, ServerFun,
			    ClientFun),
    ok.

find_boundary_test() ->
    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-----------------------------160374543510"
	  "82272548568224146">>,
    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),
    not_found = find_boundary(B, <<"\r\n--XJOPKE">>),
    ok.

find_in_binary_test() ->
    {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.

flash_parse_http_test() -> flash_parse(plain).

flash_parse_https_test() -> flash_parse(ssl).

flash_parse(Transport) ->
    ContentType =
	"multipart/form-data; boundary=----------ei4GI"
	"3GI3Ij5Ef1ae0KM7Ij5ei4Ij5",
    "----------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5" =
	get_boundary(ContentType),
    BinContent =
	<<"------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5\r\n"
	  "Content-Disposition: form-data; name=\"Filena"
	  "me\"\r\n\r\nhello.txt\r\n------------ei4GI3GI"
	  "3Ij5Ef1ae0KM7Ij5ei4Ij5\r\nContent-Disposition"
	  ": form-data; name=\"success_action_status\"\r\n\r\n"
	  "201\r\n------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei"
	  "4Ij5\r\nContent-Disposition: form-data; "
	  "name=\"file\"; filename=\"hello.txt\"\r\nCont"
	  "ent-Type: application/octet-stream\r\n\r\nhel"
	  "lo\n\r\n------------ei4GI3GI3Ij5Ef1ae0KM7Ij5e"
	  "i4Ij5\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, <<"hello\n">>}, 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, _Opts) ->
			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(Transport, ServerFun,
			    ClientFun),
    ok.

flash_parse2_http_test() -> flash_parse2(plain).

flash_parse2_https_test() -> flash_parse2(ssl).

flash_parse2(Transport) ->
    ContentType =
	"multipart/form-data; boundary=----------ei4GI"
	"3GI3Ij5Ef1ae0KM7Ij5ei4Ij5",
    "----------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5" =
	get_boundary(ContentType),
    Chunk = iolist_to_binary(string:copies("%", 4096)),
    BinContent =
	<<"------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5\r\n"
	  "Content-Disposition: form-data; name=\"Filena"
	  "me\"\r\n\r\nhello.txt\r\n------------ei4GI3GI"
	  "3Ij5Ef1ae0KM7Ij5ei4Ij5\r\nContent-Disposition"
	  ": form-data; name=\"success_action_status\"\r\n\r\n"
	  "201\r\n------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei"
	  "4Ij5\r\nContent-Disposition: form-data; "
	  "name=\"file\"; filename=\"hello.txt\"\r\nCont"
	  "ent-Type: application/octet-stream\r\n\r\n",
	  Chunk/binary,
	  "\r\n------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij"
	  "5\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, _Opts) ->
			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(Transport, ServerFun,
			    ClientFun),
    ok.

parse_headers_test() ->
    ?assertEqual([], (parse_headers(<<>>))).

flash_multipart_hack_test() ->
    Buffer = <<"prefix-">>,
    Prefix = <<"prefix">>,
    State = #mp{length = 0, buffer = Buffer,
		boundary = Prefix},
    ?assertEqual(State, (flash_multipart_hack(State))).

parts_to_body_single_test() ->
    {HL, B} = parts_to_body([{0, 5, <<"01234">>}],
			    "text/plain", 10),
    [{"Content-Range", Range}, {"Content-Type", Type}] =
	lists:sort(HL),
    ?assertEqual(<<"bytes 0-5/10">>,
		 (iolist_to_binary(Range))),
    ?assertEqual(<<"text/plain">>,
		 (iolist_to_binary(Type))),
    ?assertEqual(<<"01234">>, (iolist_to_binary(B))),
    ok.

parts_to_body_multi_test() ->
    {[{"Content-Type", Type}], _B} = parts_to_body([{0, 5,
						     <<"01234">>},
						    {5, 10, <<"56789">>}],
						   "text/plain", 10),
    ?assertMatch(<<"multipart/byteranges; boundary=",
		   _/binary>>,
		 (iolist_to_binary(Type))),
    ok.

parts_to_multipart_body_test() ->
    {[{"Content-Type", V}], B} =
	parts_to_multipart_body([{0, 5, <<"01234">>},
				 {5, 10, <<"56789">>}],
				"text/plain", 10, "BOUNDARY"),
    MB = multipart_body([{0, 5, <<"01234">>},
			 {5, 10, <<"56789">>}],
			"text/plain", "BOUNDARY", 10),
    ?assertEqual(<<"multipart/byteranges; boundary=BOUNDARY">>,
		 (iolist_to_binary(V))),
    ?assertEqual((iolist_to_binary(MB)),
		 (iolist_to_binary(B))),
    ok.

multipart_body_test() ->
    ?assertEqual(<<"--BOUNDARY--\r\n">>,
		 (iolist_to_binary(multipart_body([], "text/plain",
						  "BOUNDARY", 0)))),
    ?assertEqual(<<"--BOUNDARY\r\nContent-Type: text/plain\r\nCon"
		   "tent-Range: bytes 0-5/10\r\n\r\n01234\r\n--BO"
		   "UNDARY\r\nContent-Type: text/plain\r\nContent"
		   "-Range: bytes 5-10/10\r\n\r\n56789\r\n--BOUND"
		   "ARY--\r\n">>,
		 (iolist_to_binary(multipart_body([{0, 5, <<"01234">>},
						   {5, 10, <<"56789">>}],
						  "text/plain", "BOUNDARY",
						  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=----------ei4GI"
	"3GI3Ij5Ef1ae0KM7Ij5ei4Ij5",
    Chunk =
	binary:copy(<<"This Is_%Some=Quite0Long4String2Used9For7Benc"
		      "hmarKing.5">>,
		    102400),
    BinContent =
	<<"------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5\r\n"
	  "Content-Disposition: form-data; name=\"Filena"
	  "me\"\r\n\r\nhello.txt\r\n------------ei4GI3GI"
	  "3Ij5Ef1ae0KM7Ij5ei4Ij5\r\nContent-Disposition"
	  ": form-data; name=\"success_action_status\"\r\n\r\n"
	  "201\r\n------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei"
	  "4Ij5\r\nContent-Disposition: form-data; "
	  "name=\"file\"; filename=\"hello.txt\"\r\nCont"
	  "ent-Type: application/octet-stream\r\n\r\n",
	  Chunk/binary,
	  "\r\n------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij"
	  "5\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, _Opts) ->
			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.
