123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
/*******************************************************************************

        copyright:      Copyright (c) Nov 2007 Kris Bell. All rights reserved

        license:        BSD style: $(LICENSE)

        version:        Nov 2007: Initial release

        author:         Kris

        Support for HTTP chunked I/O.

        See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html

*******************************************************************************/

module tango.net.http.ChunkStream;

private import  tango.io.stream.Lines;

private import  tango.io.device.Conduit,
                tango.io.stream.Buffered;

private import  Integer = tango.text.convert.Integer;

/*******************************************************************************

        Prefix each block of data with its length (in hex digits) and add
        appropriate \r\n sequences. To commit the stream you'll need to use
        the terminate() function and optionally provide it with a callback
        for writing trailing headers

*******************************************************************************/

class ChunkOutput : OutputFilter
{
        private OutputBuffer output;

        /***********************************************************************

                Use a buffer belonging to our sibling, if one is available

        ***********************************************************************/

        this (OutputStream stream)
        {
                super (output = BufferedOutput.create(stream));
        }

        /***********************************************************************

                Write a chunk to the output, prefixed and postfixed in a
                manner consistent with the HTTP chunked transfer coding

        ***********************************************************************/

        final override size_t write (const(void)[] src)
        {
                char[8] tmp = void;

                output.append (Integer.format (tmp, src.length, "x"))
                      .append ("\r\n")
                      .append (src)
                      .append ("\r\n");
                return src.length;
        }

        /***********************************************************************

                Write a zero length chunk, trailing headers and a terminating
                blank line

        ***********************************************************************/

        final void terminate (scope void delegate(OutputBuffer) headers = null)
        {
                output.append ("0\r\n");
                if (headers)
                    headers (output);
                output.append ("\r\n");
        }
}


/*******************************************************************************

        Parse hex digits, and use the resultant size to modulate requests
        for incoming data. A chunk size of 0 terminates the stream, so to
        read any trailing headers you'll need to provide a delegate handler
        for receiving those

*******************************************************************************/

class ChunkInput : Lines!(char)
{
        private alias void delegate(const(char)[] line) Headers;

        private Headers         headers;
        private uint            available;

        /***********************************************************************

                Prime the available chunk size by reading and parsing the
                first available line

        ***********************************************************************/

        this (InputStream stream, Headers headers = null)
        {
                set (stream);
                this.headers = headers;
        }

        /***********************************************************************

                Reset ChunkInput to a new InputStream

        ***********************************************************************/

        override ChunkInput set (InputStream stream)
        {
                super.set (stream);
                available = nextChunk();
                return this;
        }

        /***********************************************************************

                Read content based on a previously parsed chunk size

        ***********************************************************************/

        final override size_t read (void[] dst)
        {
                if (available is 0)
                   {
                   // terminated 0 - read headers and empty line, per rfc2616
                   const(char)[] line;
                   while ((line = super.next).length)
                           if (headers)
                               headers (line);
                   return IConduit.Eof;
                   }

                auto size = dst.length > available ? available : dst.length;
                auto read = super.read (dst [0 .. size]);

                // check for next chunk header
                if (read != IConduit.Eof && (available -= read) is 0)
                   {
                   // consume trailing \r\n
                   super.input.seek (2);
                   available = nextChunk ();
                   }

                return read;
        }

        /***********************************************************************

                Read and parse another chunk size

        ***********************************************************************/

        private final uint nextChunk ()
        {
                const(char)[] tmp;

                if ((tmp = super.next).ptr)
                     return cast(uint) Integer.parse (tmp, 16);
                return 0;
        }
}


/*******************************************************************************

*******************************************************************************/

debug (ChunkStream)
{
        import tango.io.Console;
        import tango.io.device.Array;

        void main()
        {
                auto buf = new Array(40);
                auto chunk = new ChunkOutput (buf);
                chunk.write ("hello world");
                chunk.terminate;
                auto input = new ChunkInput (buf);
                Cout.stream.copy (input);
        }
}