/*
 * @(#)Codecs.java					0.2-2 23/03/1997
 *
 *  This file is part of the HTTPClient package 
 *  Copyright (C) 1996,1997  Ronald Tschalaer
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Library General Public
 *  License as published by the Free Software Foundation; either
 *  version 2 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Library General Public License for more details.
 *
 *  You should have received a copy of the GNU Library General Public
 *  License along with this library; if not, write to the Free
 *  Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
 *  MA 02111-1307, USA
 *
 *  For questions, suggestions, bug-reports, enhancement-requests etc.
 *  I may be contacted at:
 *
 *  ronald@innovation.ch
 *  Ronald.Tschalaer@psi.ch
 *
 */

package HTTPClient;


import java.util.BitSet;
import java.io.IOException;
import java.io.InputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;

/**
 * This class collects various encoders and decoders.
 *
 * @version	0.2 (bug fix 2)  23/03/1997
 * @author	Ronald Tschal&auml;r
 */

public class Codecs
{
    private static BitSet  BoundChar;
    private static byte[]  Base64EncMap, Base64DecMap;

    // Class Initializer

    static
    {	
	// rfc-1521: bcharsnospace - used for multipart codings
	BoundChar = new BitSet(256);
	for (int ch='0'; ch <= '9'; ch++)  BoundChar.set(ch);
	for (int ch='A'; ch <= 'Z'; ch++)  BoundChar.set(ch);
	for (int ch='a'; ch <= 'z'; ch++)  BoundChar.set(ch);
	BoundChar.set('\'');
	BoundChar.set('(');
	BoundChar.set(')');
	BoundChar.set('+');
	BoundChar.set(',');
	BoundChar.set('-');
	BoundChar.set('.');
	BoundChar.set('/');
	BoundChar.set(':');
	BoundChar.set('=');
	BoundChar.set('?');
	BoundChar.set('_');

	// rfc-521: Base64 Alphabet
	byte[] map =
	    {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
	     'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
	     'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
	     'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
	     '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'};
	Base64EncMap = map;
	Base64DecMap = new byte[128];
	for (int idx=0; idx<Base64EncMap.length; idx++)
	    Base64DecMap[Base64EncMap[idx]] = (byte) idx;
    }


    // Constructors

    /**
     * This class isn't meant to be instantiated.
     */
    private Codecs() {}


    // Methods

    /**
     * This method encodes the given string using the base64-encoding
     * specified in RFC-1521 (Section 5.2). It's used for example in the
     * "Basic" authorization scheme.
     *
     * @param  str the string
     * @return the base64-encoded <var>str</var>
     */
    public final static String base64Encode(String str)
    {
	if (str == null)  return  null;

	byte data[] = new byte[str.length()];
	str.getBytes(0, str.length(), data, 0);

	return new String(base64Encode(data), 0);
    }


    /**
     * This method encodes the given byte[] using the base64-encoding
     * specified in RFC-1521 (Section 5.2).
     *
     * @param  data the data
     * @return the base64-encoded <var>data</var>
     */
    public final static byte[] base64Encode(byte[] data)
    {
	if (data == null)  return  null;

	int sidx, didx;
	byte dest[] = new byte[((data.length+2)/3)*4];


	// 3-byte to 4-byte conversion + 0-63 to ascii printable conversion
	for (sidx=0, didx=0; sidx < data.length-2; sidx += 3)
	{
	    dest[didx++] = Base64EncMap[(data[sidx] >>> 2) & 077];
	    dest[didx++] = Base64EncMap[(data[sidx+1] >>> 4) & 017 |
					(data[sidx] << 4) & 077];
	    dest[didx++] = Base64EncMap[(data[sidx+2] >>> 6) & 003 |
					(data[sidx+1] << 2) & 077];
	    dest[didx++] = Base64EncMap[data[sidx+2] & 077];
	}
	if (sidx < data.length)
	{
	    dest[didx++] = Base64EncMap[(data[sidx] >>> 2) & 077];
	    if (sidx < data.length-1)
	    {
		dest[didx++] = Base64EncMap[(data[sidx+1] >>> 4) & 017 |
					    (data[sidx] << 4) & 077];
		dest[didx++] = Base64EncMap[(data[sidx+1] << 2) & 077];
	    }
	    else
		dest[didx++] = Base64EncMap[(data[sidx] << 4) & 077];
	}

	// add padding
	for ( ; didx < dest.length; didx++)
	    dest[didx] = '=';

	return dest;
    }


    /**
     * This method decodes the given string using the base64-encoding
     * specified in RFC-1521 (Section 5.2).
     *
     * @param  str the base64-encoded string.
     * @return the decoded <var>str</var>.
     */
    public final static String base64Decode(String str)
    {
	if (str == null)  return  null;

	byte data[] = new byte[str.length()];
	str.getBytes(0, str.length(), data, 0);

	return new String(base64Decode(data), 0);
    }


    /**
     * This method decodes the given byte[] using the base64-encoding
     * specified in RFC-1521 (Section 5.2).
     *
     * @param  data the base64-encoded data.
     * @return the decoded <var>data</var>.
     */
    public final static byte[] base64Decode(byte[] data)
    {
	if (data == null)  return  null;

	int tail = data.length;
	while (data[tail-1] == '=')  tail--;

	byte dest[] = new byte[tail - data.length/4];


	// ascii printable to 0-63 conversion
	for (int idx = 0; idx <data.length; idx++)
	    data[idx] = Base64DecMap[data[idx]];

	// 4-byte to 3-byte conversion
	int sidx, didx;
	for (sidx = 0, didx=0; didx < dest.length-2; sidx += 4, didx += 3)
	{
	    dest[didx]   = (byte) ( ((data[sidx] << 2) & 255) |
			    ((data[sidx+1] >>> 4) & 003) );
	    dest[didx+1] = (byte) ( ((data[sidx+1] << 4) & 255) |
			    ((data[sidx+2] >>> 2) & 017) );
	    dest[didx+2] = (byte) ( ((data[sidx+2] << 6) & 255) |
			    (data[sidx+3] & 077) );
	}
	if (didx < dest.length)
	    dest[didx]   = (byte) ( ((data[sidx] << 2) & 255) |
			    ((data[sidx+1] >>> 4) & 003) );
	if (++didx < dest.length)
	    dest[didx]   = (byte) ( ((data[sidx+1] << 4) & 255) |
			    ((data[sidx+2] >>> 2) & 017) );

	return dest;
    }


    /**
     * This method does a quoted-printable encoding of the given string
     * according to RFC-1521 (Section 5.1). Note: it assumes 8-bit characters.
     *
     * @param  str the string
     * @return the quoted-printable encoded string
     */
    public final static String quotedPrintableEncode(String str)
    {
	if (str == null)  return  null;

	char map[] =
	    {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'},
	     nl[]  = System.getProperty("line.separator", "\n").toCharArray(),
	     res[] = new char[(int) (str.length()*1.5)],
	     src[] = str.toCharArray();
	char ch;
	int  cnt  = 0,
	     didx = 1,
	     last = 0,
	     slen = str.length();


	for (int sidx=0; sidx < slen; sidx++)
	{
	    ch = src[sidx];

	    if (ch == nl[0]   &&  match(src, sidx, nl))		// Rule #4
	    {
		if (res[didx-1] == ' ')			// Rule #3
		{
		    res[didx-1] = '=';
		    res[didx++] = '2';
		    res[didx++] = '0';
		}
		else if (res[didx-1] == '\t')		// Rule #3
		{
		    res[didx-1] = '=';
		    res[didx++] = '0';
		    res[didx++] = '9';
		}

		res[didx++] = '\r';
		res[didx++] = '\n';
		sidx += nl.length - 1;
		cnt = didx;
	    }
	    else if (ch > 126  ||  (ch < 32  &&  ch != '\t')  || ch == '=')
	    {							// Rule #1, #2
		res[didx++] = '=';
		res[didx++] = map[(ch & 0xf0) >>> 4];
		res[didx++] = map[ch & 0x0f];
	    }
	    else						// Rule #1
	    {
		res[didx++] = ch;
	    }

	    if (didx > cnt+70)					// Rule #5
	    {
		res[didx++] = '=';
		res[didx++] = '\r';
		res[didx++] = '\n';
		cnt = didx;
	    }

	    if (didx > res.length-5)
		res = Util.resizeArray(res, res.length+500);
	}

	return String.valueOf(res, 1, didx-1);
    }

    private final static boolean match(char[] str, int start, char[] arr)
    {
	for (int idx=1; idx < arr.length; idx++)
	    if (str[start+idx] != arr[idx])  return false;
	return true;
    }


    /**
     * This method does a quoted-printable decoding of the given string
     * according to RFC-1521 (Section 5.1).
     *
     * @param  str the string
     * @return the decoded string
     * @exception ParseException If a '=' is not followed by a valid
     *                           2-digit hex number or '\r\n'.
     */
    public final static String quotedPrintableDecode(String str)
	    throws ParseException
    {
	if (str == null)  return  null;

	char res[] = new char[(int) (str.length()*1.1)],
	     src[] = str.toCharArray(),
	     nl[]  = System.getProperty("line.separator", "\n").toCharArray();
	int last   = 0,
	    didx   = 0,
	    slen   = str.length();


	for (int sidx=0; sidx<slen; )
	{
	    char ch = src[sidx++];

	    if (ch == '=')
	    {
		if (src[sidx] == '\n'  ||  src[sidx] == '\r')
		{					// Rule #5
		    sidx++;
		    if (src[sidx-1] == '\r'  &&
			src[sidx] == '\n')
			sidx++;
		}
		else					// Rule #1
		{
		    char repl;
		    int hi = Character.digit(src[sidx], 16),
			lo = Character.digit(src[sidx+1], 16);

		    if ((hi | lo) < 0)
			throw new ParseException(new String(src, sidx-1, 3) +
						" is an invalid code");
		    else
		    {
			repl = (char) (hi << 4 | lo);
			sidx += 2;
		    }

		    res[didx++] = repl;
		}
		last = didx;
	    }
	    else if (ch == '\n'  ||  ch == '\r')	// Rule #4
	    {
		if (src[sidx-1] == '\r'  &&  src[sidx] == '\n')
		    sidx++;
		for (int idx=0; idx<nl.length; idx++)
		    res[last++] = nl[idx];
		didx = last;
	    }
	    else					// Rule #1, #2
	    {
		res[didx++] = ch;
		if (ch != ' '  &&  ch != '\t')		// Rule #3
		    last = didx;
	    }

	    if (didx > res.length-5)
		res = Util.resizeArray(res, res.length+500);
	}

	return new String(res, 0, didx);
    }


    /**
     * This method urlencodes the given string. This method is here for
     * symmetry reasons and just calls URLEncoder.encode().
     *
     * @param  str the string
     * @return the url-encoded string
     */
    public final static String URLEncode(String str)
    {
	if (str == null)  return  null;

	return java.net.URLEncoder.encode(str);
    }


    /**
     * This method decodes the given urlencoded string.
     *
     * @param  str the url-encoded string
     * @return the decoded string
     * @exception ParseException If a '%' is not followed by a valid
     *                           2-digit hex number.
     */
    public final static String URLDecode(String str) throws ParseException
    {
	if (str == null)  return  null;

	char[] res  = new char[str.length()];
	int    didx = 0;

	for (int sidx=0; sidx<str.length(); sidx++)
	{
	    char ch = str.charAt(sidx);
	    if (ch == '+')
		res[didx++] = ' ';
	    else if (ch == '%')
	    {
		try
		{
		    res[didx++] = (char)
			Integer.parseInt(str.substring(sidx+1,sidx+3), 16);
		    sidx += 2;
		}
		catch (NumberFormatException e)
		{
		    throw new ParseException(str.substring(sidx,sidx+3) +
					    " is an invalid code");
		}
	    }
	    else
		res[didx++] = ch;
	}

	return String.valueOf(res, 0, didx);;
    }


    /**
     * This method decodes a multipart/form-data encoded string. The boundary
     * is parsed from the <var>cont_type</var> parameter, which must be of the
     * form 'multipart/form-data; boundary=...'.
     * <BR>Any encoded files are created in the directory specified by
     * <var>dir</var> using the encoded filename.
     * <BR>Note: Does not handle nested encodings (yet).
     * <BR>Example:
     * <PRE>
     *     NVPair[] opts = Codecs.mpFormDataDecode(resp.getData(),
     *                                      resp.getHeader("Content-length"),
     *                                      ".");
     * </PRE>
     * Assuming the data received looked something like:
     * <PRE>
     * -----------------------------114975832116442893661388290519
     * Content-Disposition: form-data; name="option"
     *                                                         &nbsp;
     * doit
     * -----------------------------114975832116442893661388290519
     * Content-Disposition: form-data; name="comment"; filename="comment.txt"
     *                                                         &nbsp;
     * Gnus and Gnats are not Gnomes.
     * -----------------------------114975832116442893661388290519--
     * </PRE>
     * you would get one file called <VAR>comment.txt</VAR> in the current
     * directory, and opts would contain two elements: {"option", "doit"}
     * and {"comment", "comment.txt"}
     *
     * @param     data        the form-data to decode.
     * @param     cont_type   the content type header (must contain the
     *			      boundary string).
     * @param     dir         the directory to create the files in.
     * @return                an array of name/value pairs, one for each part;
     *                        the name is the 'name' attribute given in the
     *                        Content-Disposition header; the value is either
     *                        the name of the file if a filename attribute was
     *                        found, or the contents of the part.
     * @exception IOException If any file operation fails.
     * @exception ParseException If an error during parsing occurs.
     */
    public final static NVPair[] mpFormDataDecode(byte[] data, String cont_type,
						  String dir)
	    throws IOException, ParseException
    {
	// Find and extract boundary string

	String bndstr = getParameter("boundary", cont_type);
	if (bndstr == null)
	    throw new ParseException("\'boundary\' parameter not found in Content-type: " + cont_type);

	byte[] srtbndry = new byte[bndstr.length()+4],
	       boundary = new byte[bndstr.length()+6],
	       endbndry = new byte[bndstr.length()+6];

	(    "--" + bndstr + "\r\n").getBytes(0, srtbndry.length, srtbndry, 0);
	("\r\n--" + bndstr + "\r\n").getBytes(0, boundary.length, boundary, 0);
	("\r\n--" + bndstr + "--"  ).getBytes(0, endbndry.length, endbndry, 0);


	// setup search routines

	int[] bs = Util.compile_search(srtbndry),
	      bc = Util.compile_search(boundary),
	      be = Util.compile_search(endbndry);


	// let's start parsing the actual data

	int start = Util.findStr(srtbndry, bs, data, 0, data.length);
	if (start == -1)	// didn't even find the start
	    throw new ParseException("Starting boundary not found: " +
				     new String(srtbndry,0));
	start += srtbndry.length;

	NVPair[] res  = new NVPair[10];
	boolean  done = false;
	int      idx;

	for (idx=0; !done; idx++)
	{
	    // find end of this part

	    int end = Util.findStr(boundary, bc, data, start, data.length);
	    if (end == -1)		// must be the last part
	    {
		end = Util.findStr(endbndry, be, data, start, data.length);
		if (end == -1)
		    throw new ParseException("Ending boundary not found: " +
					     new String(endbndry,0));
		done = true;
	    }

	    // parse header(s)

	    String hdr, lchdr, name=null, value, filename=null, cont_disp = null;

	    while (true)
	    {
		int next = findEOL(data, start) + 2;
		if (next-2 <= start)  break;	// empty line -> end of headers
		hdr      = new String(data, 0, start, next-2-start);
		start    = next;

		// handle line continuation
		byte ch;
		while (next < data.length-1  &&
		       ((ch = data[next]) == ' '  ||  ch == '\t'))
		{
		    next   = findEOL(data, start) + 2;
		    hdr   += new String(data, 0, start, next-2-start);
		    start  = next;
		}

		lchdr = hdr.toLowerCase();

		if (!lchdr.startsWith("content-disposition")) continue;

		int off = lchdr.indexOf("form-data", 20);
		if (off == -1)
		    throw new ParseException("Expected \'Content-Disposition: form-data\' in line: "+hdr);
		
		name = getParameter("name", hdr);
		if (name == null)
		    throw new ParseException("\'name\' parameter not found in header: "+hdr);

		filename = getParameter("filename", hdr);

		cont_disp = hdr;
	    }

	    start += 2;
	    if (start > end)
		throw new ParseException("End of header not found at offset "+end);

	    if (cont_disp == null)
		throw new ParseException("Missing \'Content-Disposition\' header at offset "+start);

	    // handle data for this part

	    if (filename != null)			// It's a file
	    {
		File file = new File(dir, filename);
		FileOutputStream out = new FileOutputStream(file);

		out.write(data, start, end-start);
		out.close();

		value = filename;
	    }
	    else					// It's simple data
	    {
		value = new String(data, 0, start, end-start);
	    }

	    if (idx >= res.length)
		res = Util.resizeArray(res, idx+10);
	    res[idx] = new NVPair(name, value);

	    start = end + boundary.length;
	}

	return Util.resizeArray(res, idx);
    }


    /**
     * retrieves the value associated with the parameter <var>param</var> in
     * a given header string. This is used especially in headers like
     * 'Content-type' and 'Content-Disposition'. Here is the syntax it
     * expects:<BR>
     * ";" <var>param</var> "=" ( token | quoted-string )
     *
     * @param  param  the parameter name
     * @param  hdr    the header value
     * @return the value for this parameter, or null if not found.
     */
    public final static String getParameter(String param, String hdr)
    {
	char ch;
	int  pbeg,	// parameter name begin
	     pend,	// parameter name end
	     vbeg,	// parameter value begin
	     vend = -1,	// parameter value end
	     len = hdr.length();


	param = param.trim();

	while (true)
	{
	    // mark parameter name

	    pbeg = hdr.toLowerCase().indexOf(';', vend+1);	// get ';'
	    if (pbeg == -1)  return null;
	    while (pbeg < len-1  &&  Character.isSpace(hdr.charAt(++pbeg))) ;
	    if (pbeg == len-1)  return null;

	    pend = hdr.toLowerCase().indexOf('=', pbeg+1);	// get '='
	    if (pend == -1)  return null;
	    vbeg = pend + 1;
	    while (Character.isSpace(hdr.charAt(--pend))); 
	    pend++;


	    // mark parameter value

	    while (vbeg < len  &&  Character.isSpace(hdr.charAt(vbeg))) vbeg++;
	    if (vbeg == len)  return null;

	    vend = vbeg;
	    if (hdr.charAt(vbeg) == '\"')		// is a quoted-string
	    {
		vbeg++;
		vend = hdr.indexOf('\"', vbeg);
		if (vend == -1)  return null;
	    }
	    else					// is a simple token
	    {
		vend = hdr.indexOf(';', vbeg);
		if (vend == -1)  vend = hdr.length();
		while (Character.isSpace(hdr.charAt(--vend))) ; 
		vend++;
	    }

	    if (hdr.regionMatches(true, pbeg, param, 0, pend-pbeg))
		break;					// found it
	}

	return hdr.substring(vbeg, vend);
    }


    /**
     * Searches for the next CRLF in an array.
     *
     * @param  arr the byte array to search.
     * @param  off the offset at which to start the search.
     * @return the position of the CR or (arr.length-2) if not found
     */
    private final static int findEOL(byte[] arr, int off)
    {
	while (off < arr.length-1  &&
	       !(arr[off++] == '\r'  &&  arr[off] == '\n'));
	return off-1;
    }

    /**
     * This method encodes name/value pairs and files into a byte array
     * using the multipart/form-data encoding. The boundary is returned
     * as part of <var>cont_type</var>.
     * <BR>Example:
     * <PRE>
     *     NVPair[] opts = { new NVPair("option", "doit") };
     *     NVPair[] file = { new NVPair("comment", "comment.txt") };
     *     NVPair[] hdrs = new NVPair[1];
     *     byte[]   data = Codecs.mpFormDataEncode(opts, file, hdrs);
     *     con.Post("/cgi-bin/handle-it", data, hdrs);
     * </PRE>
     * <VAR>data</VAR> will look something like the following:
     * <PRE>
     * -----------------------------114975832116442893661388290519
     * Content-Disposition: form-data; name="option"
     *                                                         &nbsp;
     * doit
     * -----------------------------114975832116442893661388290519
     * Content-Disposition: form-data; name="comment"; filename="comment.txt"
     *                                                         &nbsp;
     * Gnus and Gnats are not Gnomes.
     * -----------------------------114975832116442893661388290519--
     * </PRE>
     * where the "Gnus and Gnats ..." is the contents of the file
     * <VAR>comment.txt</VAR> in the current directory.
     *
     * @param     opts        the simple form-data to encode (may be null);
     *                        for each NVPair the name refers to the 'name'
     *                        attribute to be used in the header of the part,
     *                        and the value is contents of the part.
     * @param     files       the files to encode (may be null); for each
     *                        NVPair the name refers to the 'name' attribute
     *                        to be used in the header of the part, and the
     *                        value is the actual filename (the file will be
     *                        read and it's contents put in the body of that
     *                        part).
     * @param     cont_type   this returns a new NVPair in the 0'th element
     *                        which contains
     *			      name = "Content-Type",
     *			      value = "multipart/form-data; boundary=..."
     *                        (the reason this parameter is an array is
     *                        because a) that's the only way to simulate
     *                        pass-by-reference and b) you need an array for
     *                        the headers parameter to the Post() or Put()
     *                        anyway).
     * @return                an encoded byte array containing all the opts
     *			      and files.
     * @exception IOException If any file operation fails.
     */
    public final static byte[] mpFormDataEncode(NVPair[] opts, NVPair[] files,
						NVPair[] cont_type)
	    throws IOException
    {
	int len = 0,
	    hdr_len = 2  + 2 + 70 + 2 +  39 + 2 +  2;
	     //       \r\n --  bnd  \r\n C..  \r\n \r\n
	byte[] boundary  = new byte[74],
	       cont_disp = new byte[40],
	       filename  = new byte[13];

	"\r\nContent-Disposition: form-data; name=\"".getBytes(0, 40, cont_disp, 0);
	"\"; filename=\"".getBytes(0, 13, filename, 0);
	"\r\n-----ieoau=_%_+2:8?GoodLuck8)3(dskdfJwSJKlrWLr0234324jfLdsjfdAuaoei-----".getBytes(0, 74, boundary, 0);


	// Calculate the length of the data

	if (opts != null)
	{
	    for (int idx=0; idx<opts.length; idx++)
		len += hdr_len + opts[idx].getName().length() +
			opts[idx].getValue().length();
	}

	if (files != null)
	{
	    for (int idx=0; idx<files.length; idx++)
	    {
		len += hdr_len + files[idx].getName().length() + 13;

		File file = new File(files[idx].getValue());
		len += file.getName().length() + file.length();
	    }
	}

	len += 2 + 2 + 70 + 2 + 2;	// \r\n -- bnd -- \r\n


	// Now fill array

	byte[] res = new byte[len];
	int    pos = 0;

	NewBound: for (int new_c=0x30303030; new_c!=0x7A7A7A7A; new_c++)
	{
	    pos = 0;

	    // modify boundary in hopes that it will be unique
	    while (!BoundChar.get(new_c     & 0xff)) new_c += 0x00000001;
	    while (!BoundChar.get(new_c>>8  & 0xff)) new_c += 0x00000100;
	    while (!BoundChar.get(new_c>>16 & 0xff)) new_c += 0x00010000;
	    while (!BoundChar.get(new_c>>24 & 0xff)) new_c += 0x01000000;
	    boundary[40] = (byte) (new_c     & 0xff);
	    boundary[42] = (byte) (new_c>>8  & 0xff);
	    boundary[44] = (byte) (new_c>>16 & 0xff);
	    boundary[46] = (byte) (new_c>>24 & 0xff);


	    int[] bnd_cmp = Util.compile_search(boundary);

	    if (opts != null)
	    {
		for (int idx=0; idx<opts.length; idx++)
		{
		    System.arraycopy(boundary, 0, res, pos, boundary.length);
		    pos += boundary.length;
		    System.arraycopy(cont_disp, 0, res, pos, cont_disp.length);
		    pos += cont_disp.length;

		    int nlen = opts[idx].getName().length();
		    opts[idx].getName().getBytes(0, nlen, res, pos);
		    if (nlen >= boundary.length  &&
			Util.findStr(boundary, bnd_cmp, res, pos, pos+nlen) != -1)
			continue NewBound;
		    pos += nlen;

		    res[pos++] = (byte) '\"';
		    res[pos++] = (byte) '\r';
		    res[pos++] = (byte) '\n';
		    res[pos++] = (byte) '\r';
		    res[pos++] = (byte) '\n';

		    int vlen = opts[idx].getValue().length();
		    opts[idx].getValue().getBytes(0, vlen, res, pos);
		    if (vlen >= boundary.length  &&
			Util.findStr(boundary, bnd_cmp, res, pos, pos+vlen) != -1)
			continue NewBound;
		    pos += vlen;
		}
	    }

	    if (files != null)
	    {
		for (int idx=0; idx<files.length; idx++)
		{
		    File file = new File(files[idx].getValue());

		    System.arraycopy(boundary, 0, res, pos, boundary.length);
		    pos += boundary.length;
		    System.arraycopy(cont_disp, 0, res, pos, cont_disp.length);
		    pos += cont_disp.length;

		    int nlen = files[idx].getName().length();
		    files[idx].getName().getBytes(0, nlen, res, pos);
		    if (nlen >= boundary.length  &&
			Util.findStr(boundary, bnd_cmp, res, pos, pos+nlen) != -1)
			continue NewBound;
		    pos += nlen;

		    System.arraycopy(filename, 0, res, pos, filename.length);
		    pos += filename.length;

		    nlen = file.getName().length();
		    file.getName().getBytes(0, nlen, res, pos);
		    if (nlen >= boundary.length  &&
			Util.findStr(boundary, bnd_cmp, res, pos, pos+nlen) != -1)
			continue NewBound;
		    pos += nlen;

		    res[pos++] = (byte) '\"';
		    res[pos++] = (byte) '\r';
		    res[pos++] = (byte) '\n';
		    res[pos++] = (byte) '\r';
		    res[pos++] = (byte) '\n';

		    nlen = (int) file.length();
		    int opos = pos;
		    FileInputStream fin = new FileInputStream(file);
		    while (nlen > 0)
		    {
			int got = fin.read(res, pos, nlen);
			nlen -= got;
			pos += got;
		    }
		    if (Util.findStr(boundary, bnd_cmp, res, opos, pos) != -1)
			continue NewBound;
		}
	    }

	    break NewBound;
	}

	System.arraycopy(boundary, 0, res, pos, boundary.length);
	pos += boundary.length;
	res[pos++] = (byte) '-';
	res[pos++] = (byte) '-';
	res[pos++] = (byte) '\r';
	res[pos++] = (byte) '\n';

	if (pos != len)
	    throw new Error("Calculated "+len+" bytes but wrote "+pos+" bytes!");

	cont_type[0] = new NVPair("Content-Type",
				  "multipart/form-data; boundary=\"" +
				  new String(boundary, 0, 4, 70) + '\"');

	return res;
    }


    /**
     * Turns an array of name/value pairs into the string
     * "name1=value1&name2=value2&name3=value3". The names and values are
     * first urlencoded. This is the form in which form-data is passed to
     * a cgi script.
     *
     * @param pairs the array of name/value pairs
     * @return a string containg the encoded name/value pairs
     */
    public final static String nv2query(NVPair pairs[])
    {
	if (pairs == null)
	    return null;


	int          idx;
	StringBuffer qbuf = new StringBuffer();

	for (idx = 0; idx < pairs.length; idx++)
	{
	    qbuf.append(Codecs.URLEncode(pairs[idx].name) + "=" +
			Codecs.URLEncode(pairs[idx].value) + "&");
	}

	if (idx > 0)
	    qbuf.setLength(qbuf.length()-1);	// remove trailing '&'

	return qbuf.toString();
    }


    /**
     * Turns a string of the form "name1=value1&name2=value2&name3=value3"
     * into an array of name/value pairs. The names and values are
     * urldecoded. The query string is in the form in which form-data is
     * received in a cgi script.
     *
     * @param query the query string containing the encoded name/value pairs
     * @return an array of NVPairs
     * @exception ParseException If the '=' is missing in any field, or if
     *				 the urldecoding of the name or value fails
     */
    public final static NVPair[] query2nv(String query)  throws ParseException
    {
	if (query == null) return null;

	int idx = -1,
	    cnt = 1;
	while ((idx = query.indexOf('&', idx+1)) != -1)  cnt ++;
	NVPair[] pairs = new NVPair[cnt];

	for (idx=0, cnt=0; cnt<pairs.length; cnt++)
	{
	    int eq  = query.indexOf('=', idx);
	    int end = query.indexOf('&', idx);

	    if (end == -1)  end = query.length();

	    if (eq == -1  ||  eq >= end)
		throw new ParseException("\'=\' missing in " +
					 query.substring(idx, end));

	    pairs[cnt] =
		    new  NVPair(Codecs.URLDecode(query.substring(idx,eq)),
				Codecs.URLDecode(query.substring(eq+1,end)));

	    idx = end + 1;
	}

	return pairs;
    }


    /**
     * Encodes data used the chunked encoding. <var>last</var> signales if
     * this is the last chunk, in which case the appropriate footer is
     * generated.
     *
     * @param data  the data to be encoded; may be null.
     * @param ftrs  optional headers to include in the footer (ignored if
     *              not last); may be null.
     * @param last  whether this is the last chunk.
     * @return an array of bytes containing the chunk
     */
    public final static byte[] chunkedEncode(byte[] data, NVPair[] ftrs,
					     boolean last)
    {
	if (data == null) data = new byte[0];
	if (last  &&  ftrs == null) ftrs = new NVPair[0];

	// get length of data as hex-string
	String hex_len = Integer.toString(data.length, 16);


	// calculate length of chunk

	int res_len = 0;
	if (data.length > 0)	// len CRLF data CRLF
	    res_len += hex_len.length() + 2 + data.length + 2;

	if (last)
	{
	    res_len += 1 + 2;	// 0 CRLF
	    for (int idx=0; idx<ftrs.length; idx++)
		res_len += ftrs[idx].name.length() + 2 +
			   ftrs[idx].value.length() + 2; //name ': ' value CRLF
	    res_len += 2;	// CRLF
	}

	// allocate result

	byte[] res = new byte[res_len];
	int    off = 0;


	// fill result

	if (data.length > 0)
	{
	    hex_len.getBytes(0, hex_len.length(), res, off);
	    off += hex_len.length();
	    res[off++] = (byte) '\r';
	    res[off++] = (byte) '\n';

	    System.arraycopy(data, 0, res, off, data.length);
	    off += data.length;
	    res[off++] = (byte) '\r';
	    res[off++] = (byte) '\n';
	}

	if (last)
	{
	    res[off++] = (byte) '0';
	    res[off++] = (byte) '\r';
	    res[off++] = (byte) '\n';

	    for (int idx=0; idx<ftrs.length; idx++)
	    {
		ftrs[idx].name.getBytes(0, ftrs[idx].name.length(), res, off);
		off += ftrs[idx].name.length();

		res[off++] = (byte) ':';
		res[off++] = (byte) ' ';

		ftrs[idx].value.getBytes(0, ftrs[idx].value.length(), res, off);
		off += ftrs[idx].value.length();

		res[off++] = (byte) '\r';
		res[off++] = (byte) '\n';
	    }

	    res[off++] = (byte) '\r';
	    res[off++] = (byte) '\n';
	}

	if (off != res.length)
	    throw new Error("Calculated "+res.length+" bytes but wrote "+off+" bytes!");

	return res;
    }


    /**
     * Decodes chunked data. The chunks are read from an InputStream, which
     * is assumed to be correctly positioned. Use 'xxx instanceof byte[]'
     * and 'xxx instanceof NVPair[]' to determine if this was data or the
     * last chunk.
     *
     * @param  input  the stream from which to read the next chunk.
     * @return If this was a data chunk then it returns a byte[]; else
     *         it's the footer and it returns a NVPair[] containing the
     *         footers.
     * @exception ParseException If any exception during parsing occured.
     * @exception IOException    If any exception during reading occured.
     */
    public final static Object chunkedDecode(InputStream input)
	    throws ParseException, IOException
    {
	int len = getChunkLength(input);

	if (len > 0)			// it's a chunk
	{
	    byte[] res = new byte[len];

	    int off = 0;
	    while (len != -1  &&  off < res.length)
	    {
		len  = input.read(res, off, res.length-off);
		off += len;
	    }

	    if (len == -1)
		throw new ParseException("Premature EOF while reading chunk;" +
					 "Expected: "+res.length+" Bytes, " +
					 "Received: "+(off+1)+" Bytes");

	    input.read();	// CR
	    input.read();	// LF

	    return res;
	}
	else				// it's the end
	{
	    NVPair[] res = new NVPair[0];

	    DataInputStream datain = new DataInputStream(input);
	    String line;

	    // read and parse footer
	    while ((line = datain.readLine()) != null  &&  line.length() > 0)
	    {
		int colon = line.indexOf(':');
		if (colon == -1)
		    throw new ParseException("Error in Footer format: no "+
					     "\':\' found in \'" + line + "\'");
		res = Util.resizeArray(res, res.length+1);
		res[res.length-1] = new NVPair(line.substring(0, colon).trim(),
					       line.substring(colon+1).trim());
	    }

	    return res;
	}

    }


    /**
     * Gets the length of the chunk.
     *
     * @param  input  the stream from which to read the next chunk.
     * @return  the length of chunk to follow (w/o trailing CR LF).
     * @exception ParseException If any exception during parsing occured.
     * @exception IOException    If any exception during reading occured.
     */
    final static int getChunkLength(InputStream input)
	    throws ParseException, IOException
    {
	byte[] hex_len = new byte[8];	// if they send more than 2GB chunks...
	int    off     = 0,
	       ch;


	// read chunk length

	while ((ch = input.read()) != '\r'  &&  ch != '\n'  &&
		ch != ';'  &&  off < hex_len.length)
	    hex_len[off++] = (byte) ch;
	
	if (ch == ';')		// chunk-ext (ignore it)
	    while ((ch = input.read()) != '\r'  &&  ch != '\n') ;

	if (ch != '\n'  &&  (ch != '\r'  ||  input.read() != '\n'))
	    throw new ParseException("Didn't find valid chunk length: " +
				     new String(hex_len, 0, 0, off));

	// parse chunk length

	int len;
	try
	    { len = Integer.parseInt(new String(hex_len, 0, 0, off).trim(),
				     16); }
	catch (NumberFormatException nfe)
	    { throw new ParseException("Didn't find valid chunk length: " +
					new String(hex_len, 0, 0, off) ); }

	return len;
    }

}

