/* **************************************************************************
 * $OpenLDAP: /com/novell/sasl/client/DirectiveList.java,v 1.4 2005/01/17 15:00:54 sunilk Exp $
 *
 * Copyright (C) 2002 Novell, Inc. All Rights Reserved.
 *
 * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND
 * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT
 * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS
 * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE"
 * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION
 * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP
 * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT
 * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY.
 ******************************************************************************/
package com.novell.sasl.client;

import java.util.*;
import org.apache.harmony.javax.security.sasl.*;
import java.io.UnsupportedEncodingException;

/**
 * Implements the DirectiveList class whihc will be used by the 
 * DigestMD5SaslClient class
 */
class DirectiveList extends Object
{
    private static final int STATE_LOOKING_FOR_FIRST_DIRECTIVE  = 1;
    private static final int STATE_LOOKING_FOR_DIRECTIVE        = 2;
    private static final int STATE_SCANNING_NAME                = 3;
    private static final int STATE_LOOKING_FOR_EQUALS            = 4;
    private static final int STATE_LOOKING_FOR_VALUE            = 5;
    private static final int STATE_LOOKING_FOR_COMMA            = 6;
    private static final int STATE_SCANNING_QUOTED_STRING_VALUE    = 7;
    private static final int STATE_SCANNING_TOKEN_VALUE            = 8;
    private static final int STATE_NO_UTF8_SUPPORT              = 9;

    private int        m_curPos;
    private int        m_errorPos;
    private String     m_directives;
    private int        m_state;
    private ArrayList  m_directiveList;
    private String     m_curName;
    private int        m_scanStart;

    /**
     *  Constructs a new DirectiveList.
     */
     DirectiveList(
        byte[] directives)
    {
        m_curPos = 0;
        m_state = STATE_LOOKING_FOR_FIRST_DIRECTIVE;
        m_directiveList = new ArrayList(10);
        m_scanStart = 0;
        m_errorPos = -1;
        try
        {
            m_directives = new String(directives, "UTF-8");
        }
        catch(UnsupportedEncodingException e)
        {
            m_state = STATE_NO_UTF8_SUPPORT;
        }
    }

    /**
     * This function takes a US-ASCII character string containing a list of comma
     * separated directives, and parses the string into the individual directives
     * and their values. A directive consists of a token specifying the directive
     * name followed by an equal sign (=) and the directive value. The value is
     * either a token or a quoted string
     *
     * @exception SaslException  If an error Occurs
     */
    void parseDirectives() throws SaslException
    {
        char        prevChar;
        char        currChar;
        int            rc = 0;
        boolean        haveQuotedPair = false;
        String      currentName = "<no name>";

        if (m_state == STATE_NO_UTF8_SUPPORT)
            throw new SaslException("No UTF-8 support on platform");

        prevChar = 0;

        while (m_curPos < m_directives.length())
        {
            currChar = m_directives.charAt(m_curPos);
            switch (m_state)
            {
            case STATE_LOOKING_FOR_FIRST_DIRECTIVE:
            case STATE_LOOKING_FOR_DIRECTIVE:
                if (isWhiteSpace(currChar))
                {
                    break;
                }
                else if (isValidTokenChar(currChar))
                {
                    m_scanStart = m_curPos;
                    m_state = STATE_SCANNING_NAME;
                }
                else
                {
                     m_errorPos = m_curPos;
                    throw new SaslException("Parse error: Invalid name character");
                }
                break;

            case STATE_SCANNING_NAME:
                if (isValidTokenChar(currChar))
                {
                    break;
                }
                else if (isWhiteSpace(currChar))
                {
                    currentName = m_directives.substring(m_scanStart, m_curPos);
                    m_state = STATE_LOOKING_FOR_EQUALS;
                }
                else if ('=' == currChar)
                {
                    currentName = m_directives.substring(m_scanStart, m_curPos);
                    m_state = STATE_LOOKING_FOR_VALUE;
                }
                else
                {
                     m_errorPos = m_curPos;
                    throw new SaslException("Parse error: Invalid name character");
                }
                break;

            case STATE_LOOKING_FOR_EQUALS:
                if (isWhiteSpace(currChar))
                {
                    break;
                }
                else if ('=' == currChar)
                {
                    m_state = STATE_LOOKING_FOR_VALUE;
                }
                else
                {
                    m_errorPos = m_curPos;
                    throw new SaslException("Parse error: Expected equals sign '='.");
                }
                break;

            case STATE_LOOKING_FOR_VALUE:
                if (isWhiteSpace(currChar))
                {
                    break;
                }
                else if ('"' == currChar)
                {
                    m_scanStart = m_curPos+1; /* don't include the quote */
                    m_state = STATE_SCANNING_QUOTED_STRING_VALUE;
                }
                else if (isValidTokenChar(currChar))
                {
                    m_scanStart = m_curPos;
                    m_state = STATE_SCANNING_TOKEN_VALUE;
                }
                else
                {
                    m_errorPos = m_curPos;
                    throw new SaslException("Parse error: Unexpected character");
                }
                break;

            case STATE_SCANNING_TOKEN_VALUE:
                if (isValidTokenChar(currChar))
                {
                    break;
                }
                else if (isWhiteSpace(currChar))
                {
                    addDirective(currentName, false);
                    m_state = STATE_LOOKING_FOR_COMMA;
                }
                else if (',' == currChar)
                {
                    addDirective(currentName, false);
                    m_state = STATE_LOOKING_FOR_DIRECTIVE;
                }
                else
                {
                     m_errorPos = m_curPos;
                    throw new SaslException("Parse error: Invalid value character");
                }
                break;

            case STATE_SCANNING_QUOTED_STRING_VALUE:
                if ('\\' == currChar)
                    haveQuotedPair = true;
                if ( ('"' == currChar) &&
                     ('\\' != prevChar) )
                {
                    addDirective(currentName, haveQuotedPair);
                    haveQuotedPair = false;
                    m_state = STATE_LOOKING_FOR_COMMA;
                }
                break;

            case STATE_LOOKING_FOR_COMMA:
                if (isWhiteSpace(currChar))
                    break;
                else if (currChar == ',')
                    m_state = STATE_LOOKING_FOR_DIRECTIVE;
                else
                {
                    m_errorPos = m_curPos;
                    throw new SaslException("Parse error: Expected a comma.");
                }
                break;
            }
            if (0 != rc)
                break;
            prevChar = currChar;
            m_curPos++;
        } /* end while loop */


        if (rc == 0)
        {
            /* check the ending state */
            switch (m_state)
            {
            case STATE_SCANNING_TOKEN_VALUE:
                addDirective(currentName, false);
                break;

            case STATE_LOOKING_FOR_FIRST_DIRECTIVE:
            case STATE_LOOKING_FOR_COMMA:
                break;

            case STATE_LOOKING_FOR_DIRECTIVE:
                    throw new SaslException("Parse error: Trailing comma.");

            case STATE_SCANNING_NAME:
            case STATE_LOOKING_FOR_EQUALS:
            case STATE_LOOKING_FOR_VALUE:
                    throw new SaslException("Parse error: Missing value.");

            case STATE_SCANNING_QUOTED_STRING_VALUE:
                    throw new SaslException("Parse error: Missing closing quote.");
            }
        }

    }

    /**
     * This function returns TRUE if the character is a valid token character.
     *
     *     token          = 1*<any CHAR except CTLs or separators>
     *
     *      separators     = "(" | ")" | "<" | ">" | "@"
     *                     | "," | ";" | ":" | "\" | <">
     *                     | "/" | "[" | "]" | "?" | "="
     *                     | "{" | "}" | SP | HT
     *
     *      CTL            = <any US-ASCII control character
     *                       (octets 0 - 31) and DEL (127)>
     *
     *      CHAR           = <any US-ASCII character (octets 0 - 127)>
     *
     * @param c  character to be tested
     *
     * @return Returns TRUE if the character is a valid token character.
     */
    boolean isValidTokenChar(
        char c)
    {
        if ( ( (c >= '\u0000') && (c <='\u0020') ) ||
             ( (c >= '\u003a') && (c <= '\u0040') ) ||
             ( (c >= '\u005b') && (c <= '\u005d') ) ||
             ('\u002c' == c) ||
             ('\u0025' == c) ||
             ('\u0028' == c) ||
             ('\u0029' == c) ||
             ('\u007b' == c) ||
             ('\u007d' == c) ||
             ('\u007f' == c) )
            return false;

        return true;
    }

    /**
     * This function returns TRUE if the character is linear white space (LWS).
     *         LWS = [CRLF] 1*( SP | HT )
     * @param c  Input charcter to be tested
     *
     * @return Returns TRUE if the character is linear white space (LWS)
     */
    boolean isWhiteSpace(
        char c)
    {
        if ( ('\t' == c) ||  // HORIZONTAL TABULATION.
             ('\n' == c) ||  // LINE FEED.
             ('\r' == c) ||  // CARRIAGE RETURN.
             ('\u0020' == c) )
            return true;

        return false;
    }

    /**
     * This function creates a directive record and adds it to the list, the
     * value will be added later after it is parsed.
     *
     * @param name  Name
     * @param haveQuotedPair true if quoted pair is there else false
     */
    void addDirective(
        String    name,
        boolean   haveQuotedPair)
    {
        String value;
        int    inputIndex;
        int    valueIndex;
        char   valueChar;
        int    type;

        if (!haveQuotedPair)
        {
            value = m_directives.substring(m_scanStart, m_curPos);
        }
        else
        { //copy one character at a time skipping backslash excapes.
            StringBuffer valueBuf = new StringBuffer(m_curPos - m_scanStart);
            valueIndex = 0;
            inputIndex = m_scanStart;
            while (inputIndex < m_curPos)
            {
                if ('\\' == (valueChar = m_directives.charAt(inputIndex)))
                    inputIndex++;
                valueBuf.setCharAt(valueIndex, m_directives.charAt(inputIndex));
                valueIndex++;
                inputIndex++;
            }
            value = new String(valueBuf);
        }

        if (m_state == STATE_SCANNING_QUOTED_STRING_VALUE)
            type = ParsedDirective.QUOTED_STRING_VALUE;
        else
            type = ParsedDirective.TOKEN_VALUE;
        m_directiveList.add(new ParsedDirective(name, value, type));
    }


    /**
     * Returns the List iterator.
     *
     * @return     Returns the Iterator Object for the List.
     */
    Iterator getIterator()
    {
        return m_directiveList.iterator();
    }
}