// --- BEGIN COPYRIGHT BLOCK ---
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; version 2 of the License.
//
// This program 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// (C) 2007 Red Hat, Inc.
// All rights reserved.
// --- END COPYRIGHT BLOCK ---
package netscape.security.x509;

import java.io.ByteArrayOutputStream;
import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.PushbackReader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Arrays;
import java.util.Vector;

import netscape.security.util.DerValue;
import netscape.security.util.ObjectIdentifier;

/**
 * A converter that converts Ldap v3 DN strings as specified in
 * draft-ietf-asid-ldapv3-dn-03.txt to a X500Name, RDN or AVA and
 * vice versa.
 *
 * @see LdapDNStrConverter
 * @see X500Name
 * @see RDN
 * @see AVA
 * @see X500NameAttrMap
 *
 * @author Lily Hsiao, Slava Galperin at Netscape Communications, Inc.
 */

public class LdapV3DNStrConverter extends LdapDNStrConverter {
    //
    // Constructors
    //

    /**
     * Constructs a LdapV3DNStrConverter using the global default
     * X500NameAttrMap and accept OIDs not in the default X500NameAttrMap.
     *
     * @see X500NameAttrMap
     */
    public LdapV3DNStrConverter() {
        attrMap = X500NameAttrMap.getDefault();

        acceptUnknownOids = true;
    }

    /**
     * Constructs a LdapV3DNStrConverter using the specified X500NameAttrMap
     * and a boolean indicating whether to accept OIDs not listed in the
     * X500NameAttrMap.
     *
     * @param attributeMap a X500NameAttrMap
     * @param doAcceptUnknownOids whether to convert unregistered OIDs
     *            (oids not in the X500NameAttrMap)
     * @see X500NameAttrMap
     */
    public LdapV3DNStrConverter(X500NameAttrMap attributeMap,
                boolean doAcceptUnknownOids) {
        attrMap = attributeMap;
        acceptUnknownOids = doAcceptUnknownOids;

    }

    //
    // public parsing methods
    // From LdapDNStrConverter interface
    //

    /**
     * Parse a Ldap v3 DN string to a X500Name.
     *
     * @param dn a LDAP v3 DN String
     * @return a X500Name
     * @exception IOException if an error occurs during the conversion.
     */
    public X500Name parseDN(String dn)
            throws IOException {
        return parseDN(dn, null);
    }

    /**
     * Like parseDN(String) with a DER encoding order given as argument for
     * Directory Strings.
     */
    public X500Name parseDN(String dn, byte[] encodingOrder)
            throws IOException {
        StringReader dn_reader = new StringReader(dn);
        PushbackReader in = new PushbackReader(dn_reader, 5);

        return parseDN(in, encodingOrder);
    }

    /**
     * Parse a Ldap v3 DN string with a RDN component to a RDN
     *
     * @param rdn a LDAP v3 DN String
     * @return a RDN
     * @exception IOException if an error occurs during the conversion.
     */
    public RDN parseRDN(String rdn)
            throws IOException {
        return parseRDN(rdn, null);
    }

    /**
     * Like parseRDN(String) with a DER encoding order given as argument for
     * Directory Strings.
     */
    public RDN parseRDN(String rdn, byte[] encodingOrder)
            throws IOException {
        StringReader rdn_reader = new StringReader(rdn);
        PushbackReader in = new PushbackReader(rdn_reader, 5);

        return parseRDN(in, null);
    }

    /**
     * Parse a Ldap v3 DN string with a AVA component to a AVA.
     *
     * @param ava a LDAP v3 DN string
     * @return a AVA
     */
    public AVA parseAVA(String ava)
            throws IOException {
        return parseAVA(ava, null);
    }

    /**
     * Like parseDN(String) with a DER encoding order given as argument for
     * Directory Strings.
     */
    public AVA parseAVA(String ava, byte[] encodingOrder)
            throws IOException {
        StringReader ava_reader = new StringReader(ava);
        PushbackReader in = new PushbackReader(ava_reader, 5);

        return parseAVA(in, encodingOrder);
    }

    //
    // public parsing methods called by other methods.
    //

    /**
     * Parses a Ldap DN string in a string reader to a X500Name.
     *
     * @param in Pushback string reader for a Ldap DN string.
     *            The pushback reader must have a pushback buffer size > 2.
     *
     * @return a X500Name
     *
     * @exception IOException if any reading or parsing error occurs.
     */
    public X500Name parseDN(PushbackReader in)
            throws IOException {
        return parseDN(in, null);
    }

    /**
     * Like parseDN(PushbackReader in) with a DER encoding order given as
     * argument for Directory Strings.
     */
    public X500Name parseDN(PushbackReader in, byte[] encodingOrder)
            throws IOException {
        RDN rdn;
        int lastChar;
        Vector<RDN> rdnVector = new Vector<RDN>();
        RDN names[];
        int i, j;

        do {
            rdn = parseRDN(in, encodingOrder);
            rdnVector.addElement(rdn);
            lastChar = in.read();
        } while (lastChar == ',' || lastChar == ';');

        names = new RDN[rdnVector.size()];
        for (i = 0, j = rdnVector.size() - 1; i < rdnVector.size(); i++, j--)
            names[j] = rdnVector.elementAt(i);
        return new X500Name(names);
    }

    /**
     * Parses Ldap DN string with a rdn component
     * from a string reader to a RDN. The string reader will point
     * to the separator after the rdn component or -1 if at end of string.
     *
     * @param in Pushback string reader containing a Ldap DN string with
     *            at least one rdn component.
     *            The pushback reader must have a pushback buffer size > 2.
     *
     * @return RDN object of the first rdn component in the Ldap DN string.
     *
     * @exception IOException if any read or parse error occurs.
     */
    public RDN parseRDN(PushbackReader in)
            throws IOException {
        return parseRDN(in, null);
    }

    /**
     * Like parseRDN(PushbackReader) with a DER encoding order given as
     * argument for Directory Strings.
     */
    public RDN parseRDN(PushbackReader in, byte[] encodingOrder)
            throws IOException {
        Vector<AVA> avaVector = new Vector<AVA>();
        AVA ava;
        int lastChar;
        AVA assertion[];

        do {
            ava = parseAVA(in, encodingOrder);
            avaVector.addElement(ava);
            lastChar = in.read();
        } while (lastChar == '+');

        if (lastChar != -1)
            in.unread(lastChar);

        assertion = new AVA[avaVector.size()];
        for (int i = 0; i < avaVector.size(); i++)
            assertion[i] = avaVector.elementAt(i);
        return new RDN(assertion);
    }

    /**
     * Parses a Ldap DN string with a AVA component
     * from a string reader to an AVA. The string reader will point
     * to the AVA separator after the ava string or -1 if end of string.
     *
     * @param in a Pushback reader containg a Ldap string with
     *            at least one AVA component.
     *            The Pushback reader must have a pushback buffer size > 2.
     *
     * @return AVA object of the first AVA component in the Ldap DN string.
     */
    public AVA parseAVA(PushbackReader in)
            throws IOException {
        return parseAVA(in, null);
    }

    /**
     * Like parseAVA(PushbackReader) with a DER encoding order given as
     * argument for Directory Strings.
     */
    public AVA parseAVA(PushbackReader in, byte[] encodingOrder)
            throws IOException {
        int c;
        ObjectIdentifier oid;
        DerValue value;
        StringBuffer keywordBuf;
        StringBuffer valueBuf;
        ByteArrayOutputStream berStream;
        char hexChar1, hexChar2;
        CharArrayWriter hexCharsBuf;
        String endChars;

        /* First get the keyword indicating the attribute's type,
         * and map it to the appropriate OID.
         */
        keywordBuf = new StringBuffer();
        for (;;) {
            c = in.read();
            if (c == '=')
                break;
            if (c == -1) {
                throw new IOException("Bad AVA format: Missing '='");
            }
            keywordBuf.append((char) c);
        }
        oid = parseAVAKeyword(keywordBuf.toString());

        /* Now parse the value.  "#hex", a quoted string, or a string
             * terminated by "+", ",", ";", ">".  Whitespace before or after
             * the value is stripped.
             */
        for (c = in.read(); c == ' '; c = in.read())
            continue;
        if (c == -1)
            throw new IOException("Bad AVA format: Missing attribute value");

        if (c == '#') {
            /*
             * NOTE per LDAPv3 dn string ietf standard the value represented
             * by this form is a BER value. But we only support DER value here
             * which is only a form of BER.
             */
            berStream = new ByteArrayOutputStream();
            int b;
            for (;;) {
                hexChar1 = (char) (c = in.read());
                if (c == -1 || octoEndChars.indexOf(c) > 0) // end of value
                    break;
                hexChar2 = (char) (c = in.read());
                if (hexDigits.indexOf(hexChar1) == -1 ||
                        hexDigits.indexOf(hexChar2) == -1)
                    throw new IOException("Bad AVA value: bad hex value.");
                b = (Character.digit(hexChar1, 16) << 4) +
                        Character.digit(hexChar2, 16);
                berStream.write(b);
            }
            if (berStream.size() == 0)
                throw new IOException("bad AVA format: invalid hex value");

            value = parseAVAValue(berStream.toByteArray(), oid);

            while (c == ' ' && c != -1)
                c = in.read();
        } else {
            valueBuf = new StringBuffer();
            boolean quoted = false;
            if (c == '"') {
                quoted = true;
                endChars = quotedEndChars;
                if ((c = in.read()) == -1)
                    throw new IOException("Bad AVA format: Missing attrValue");
            } else {
                endChars = valueEndChars;
            }

            // QUOTATION * ( quotechar / pair ) QUOTATION
            // quotechar = any character except '\' or QUOTATION
            // pair = '\' ( special | '\' | QUOTATION | hexpair )
            while (c != -1 && endChars.indexOf(c) == -1) {
                if (c == '\\') {
                    if ((c = in.read()) == -1)
                        throw new IOException("Bad AVA format: expecting " +
                                              "escaped char.");
                    // expect escaping of special chars, space and CR.
                    if (specialChars.indexOf((char) c) != -1 || c == '\n' ||
                            c == '\\' || c == '"' || c == ' ') {
                        valueBuf.append((char) c);
                    } else if (hexDigits.indexOf(c) != -1) {
                        hexCharsBuf = new CharArrayWriter();
                        // handle sequence of '\' hexpair
                        do {
                            hexChar1 = (char) c;
                            hexChar2 = (char) (c = in.read());
                            if (hexDigits.indexOf((char) c) == -1)
                                throw new IOException("Bad AVA format: " +
                                        "invalid escaped hex pair");
                            hexCharsBuf.write(hexChar1);
                            hexCharsBuf.write(hexChar2);
                            // read ahead to next '\' hex-char if any.
                            if ((c = in.read()) == -1)
                                break;
                            if (c != '\\') {
                                in.unread(c);
                                break;
                            }
                            if ((c = in.read()) == -1)
                                throw new IOException("Bad AVA format: " +
                                        "expecting escaped char.");
                            if (hexDigits.indexOf((char) c) == -1) {
                                in.unread(c);
                                in.unread('\\');
                                break;
                            }
                        } while (true);
                        valueBuf.append(
                                getStringFromHexpairs(hexCharsBuf.toCharArray()));
                    } else {
                        throw new IOException("Bad AVA format: " +
                                              "invalid escaping");
                    }
                } else
                    valueBuf.append((char) c);
                c = in.read();
            }

            value = parseAVAValue(
                    valueBuf.toString().trim(), oid, encodingOrder);

            if (quoted) { // move to next non-white space
                do {
                    c = in.read();
                } while (c == ' ');
                if (c != -1 && valueEndChars.indexOf(c) == -1)
                    throw new IOException(
                            "Bad AVA format: separator expected at end of ava.");
            }
        }

        if (c != -1)
            in.unread(c);

        return new AVA(oid, value);
    }

    /**
     * Converts a AVA keyword from a Ldap DN string to an ObjectIdentifier
     * from the attribute map or, if this keyword is an OID not
     * in the attribute map, create a new ObjectIdentifier for the keyword
     * if acceptUnknownOids is true.
     *
     * @param avaKeyword AVA keyword from a Ldap DN string.
     *
     * @return a ObjectIdentifier object
     * @exception IOException if the keyword is an OID not in the attribute
     *                map and acceptUnknownOids is false, or
     *                if an error occurs during conversion.
     */
    public ObjectIdentifier parseAVAKeyword(String avaKeyword)
            throws IOException {
        String keyword = avaKeyword.toUpperCase().trim();
        String oid_str = null;
        ObjectIdentifier oid, new_oid;

        if (Character.digit(keyword.charAt(0), 10) != -1) {
            // value is an oid string of 1.2.3.4
            oid_str = keyword;
        } else if (keyword.startsWith("oid.") || keyword.startsWith("OID.")) {
            // value is an oid string of oid.1.2.3.4 or OID.1.2...
            oid_str = keyword.substring(4);
        }

        if (oid_str != null) {
            // value is an oid string of 1.2.3.4 or oid.1.2.3.4 or OID.1.2...
            new_oid = new ObjectIdentifier(oid_str);
            oid = attrMap.getOid(new_oid);
            if (oid == null) {
                if (!acceptUnknownOids)
                    throw new IOException("Unknown AVA OID.");
                oid = new_oid;
            }
        } else {
            oid = attrMap.getOid(keyword);
            if (oid == null)
                throw new IOException("Unknown AVA keyword '" + keyword + "'.");
        }

        return oid;
    }

    /**
     * Converts a AVA value from a Ldap dn string to a
     * DerValue according the attribute type. For example, a value for
     * CN, OU or O is expected to be a Directory String and will be converted
     * to a DerValue of ASN.1 type PrintableString, T61String or
     * UniversalString. A Directory String is a ASN.1 CHOICE of Printable,
     * T.61 or Universal string.
     *
     * @param avaValueString a attribute value from a Ldap DN string.
     * @param oid OID of the attribute.
     *
     * @return DerValue for the value.
     *
     * @exception IOException if an error occurs during conversion.
     * @see AVAValueConverter
     */
    public DerValue parseAVAValue(String avaValueString, ObjectIdentifier oid)
            throws IOException {
        return parseAVAValue(avaValueString, oid, null);
    }

    /**
     * Like parseAVAValue(String) with a DER encoding order given as argument
     * for Directory Strings.
     */
    public DerValue parseAVAValue(
            String avaValueString, ObjectIdentifier oid, byte[] encodingOrder)
            throws IOException {
        AVAValueConverter valueConverter = attrMap.getValueConverter(oid);
        if (valueConverter == null) {
            if (!acceptUnknownOids) {
                throw new IllegalArgumentException(
                        "Unrecognized OID for AVA value conversion");
            } else {
                valueConverter = new GenericValueConverter();
            }
        }
        return valueConverter.getValue(avaValueString, encodingOrder);
    }

    /**
     * Converts a value in BER encoding, for example given in octothorpe form
     * in a Ldap v3 dn string, to a DerValue. Checks if the BER encoded value
     * is a legal value for the attribute.
     * <p>
     * <strong><i>NOTE:</i></strong> only DER encoded values are supported for the BER encoded value.
     *
     * @param berValue a value in BER encoding
     * @param oid ObjectIdentifier of the attribute.
     *
     * @return DerValue for the BER encoded value
     * @exception IOException if an error occurs during conversion.
     */
    public DerValue parseAVAValue(byte[] berValue, ObjectIdentifier oid)
            throws IOException {
        AVAValueConverter valueConverter = attrMap.getValueConverter(oid);
        if (valueConverter == null && !acceptUnknownOids) {
            throw new IllegalArgumentException(
                    "Unrecognized OID for AVA value conversion");
        } else {
            valueConverter = new GenericValueConverter();
        }
        return valueConverter.getValue(berValue);
    }

    //
    // public encoding methods.
    //

    /**
     * Converts a X500Name object to a Ldap v3 DN string (except in unicode).
     *
     * @param x500name a X500Name
     *
     * @return a Ldap v3 DN String (except in unicode).
     *
     * @exception IOException if an error is encountered during conversion.
     */
    public String encodeDN(X500Name x500name)
            throws IOException {
        RDN[] rdns = x500name.getNames();
        // String fullname = null;
        StringBuffer fullname = new StringBuffer();
        String s;
        int i;
        if (rdns.length == 0)
            return "";
        i = rdns.length - 1;
        fullname.append(encodeRDN(rdns[i--]));
        while (i >= 0) {
            s = encodeRDN(rdns[i--]);
            fullname.append(",");
            fullname.append(s);
        }
        ;
        return fullname.toString();
    }

    /**
     * Converts a RDN to a Ldap v3 DN string (except in unicode).
     *
     * @param rdn a RDN
     *
     * @return a LDAP v3 DN string (except in unicode).
     *
     * @exception IOException if an error is encountered during conversion.
     */
    public String encodeRDN(RDN rdn)
            throws IOException {
        AVA[] avas = rdn.getAssertion();
        // String relname = null;
        StringBuffer relname = new StringBuffer();
        String s;
        int i = 0;

        relname.append(encodeAVA(avas[i++]));
        while (i < avas.length) {
            s = encodeAVA(avas[i++]);
            relname.append("+");
            relname.append(s);
        }
        ;
        return relname.toString();
    }

    /**
     * Converts a AVA to a Ldap v3 DN String (except in unicode).
     *
     * @param ava an AVA
     *
     * @return a Ldap v3 DN string (except in unicode).
     *
     * @exception IOException If an error is encountered during exception.
     */
    public String encodeAVA(AVA ava)
            throws IOException {
        if (ava == null) {
            return "";
        }
        ObjectIdentifier oid = ava.getOid();
        DerValue value = ava.getValue();
        String keyword, valueStr;

        // get attribute name

        keyword = encodeOID(oid);
        valueStr = encodeValue(value, oid);

        return keyword + "=" + valueStr;
    }

    /**
     * Converts an OID to a attribute keyword in a Ldap v3 DN string
     * - either a keyword if known or a string of "1.2.3.4" syntax.
     *
     * @param oid a ObjectIdentifier
     *
     * @return a keyword to use in a Ldap V3 DN string.
     *
     * @exception IOException if an error is encountered during conversion.
     */
    public String encodeOID(ObjectIdentifier oid)
            throws IOException {
        String keyword = attrMap.getName(oid);
        if (keyword == null) {
            if (acceptUnknownOids)
                keyword = oid.toString();
            else
                throw new IOException("Unknown OID");
        }
        return keyword;
    }

    /**
     * Converts a value as a DerValue to a string in a Ldap V3 DN String.
     * If the value cannot be converted to a string it will be encoded in
     * octothorpe form.
     *
     * @param attrValue a value as a DerValue.
     * @param oid OID for the attribute.
     * @return a string for the value in a LDAP v3 DN String
     * @exception IOException if an error occurs during conversion.
     */
    public String encodeValue(DerValue attrValue, ObjectIdentifier oid)
            throws IOException {
        /*
         * Construct the value with as little copying and garbage
         * production as practical.
         */
        StringBuffer retval = new StringBuffer(30);
        int i;
        String temp = null;
        AVAValueConverter valueConverter;

        X500NameAttrMap lAttrMap = attrMap;

        if (attrValue.tag == DerValue.tag_UTF8String) {
            lAttrMap = X500NameAttrMap.getDirDefault();

        }

        valueConverter = lAttrMap.getValueConverter(oid);
        if (valueConverter == null) {
            if (acceptUnknownOids)
                valueConverter = new GenericValueConverter();
            else
                throw new IOException(
                        "Unknown AVA type for encoding AVA value");
        }

        try {
            temp = valueConverter.getAsString(attrValue);

            if (temp == null) {
                // convert to octothorpe form.
                byte data[] = attrValue.toByteArray();

                retval.append('#');
                for (i = 0; i < data.length; i++) {
                    retval.append(hexDigits.charAt((data[i] >> 4) & 0x0f));
                    retval.append(hexDigits.charAt(data[i] & 0x0f));
                }

            } else {

                retval.append(encodeString(temp));

            }
        } catch (IOException e) {
            throw new IllegalArgumentException("malformed AVA DER Value");
        }

        return retval.toString();
    }

    /**
     * converts a raw value string to a string in Ldap V3 DN string format.
     *
     * @param valueStr a 'raw' value string.
     * @return a attribute value string in Ldap V3 DN string format.
     */
    public String encodeString(String valueStr) {
        int i, j;
        int len;
        StringBuffer retval = new StringBuffer();

        /*
         * generate string according to ldapv3 DN. escaping is used.
         * Strings generated this way are acceptable by rfc1779
         * implementations.
         */
        len = valueStr.length();

        // get index of first space at the end of the string.
        for (j = len - 1; j >= 0 && valueStr.charAt(j) == ' '; j--)
            continue;

        // escape spaces at the beginning of the string.
        for (i = 0; i <= j && valueStr.charAt(i) == ' '; i++) {
            retval.append('\\');
            retval.append(valueStr.charAt(i));
        }

        // escape special characters in the middle of the string.
        for (; i <= j; i++) {
            if (valueStr.charAt(i) == '\\') {
                retval.append('\\');
                retval.append(valueStr.charAt(i));
            } else if (specialChars.indexOf(valueStr.charAt(i)) != -1) {
                retval.append('\\');
                retval.append(valueStr.charAt(i));
            } else if (valueStr.charAt(i) == '"') {
                retval.append('\\');
                retval.append(valueStr.charAt(i));
            } else
                retval.append(valueStr.charAt(i));
        }

        // esacape spaces at the end.
        for (; i < valueStr.length(); i++) {
            retval.append('\\');
            retval.append(' ');
        }

        return retval.toString();
    }

    //
    // public get/set methods
    //

    /**
     * gets the X500NameAttrMap used by the converter.
     *
     * @return X500NameAttrMap used by this converter.
     */
    public X500NameAttrMap getAttrMap() {
        return attrMap;
    }

    /**
     * returns true if the converter accepts unregistered attributes i.e.
     * OIDS not in the X500NameAttrMap.
     *
     * @return true if converter converts attributes not in the
     *         X500NameAttrMap.
     */
    public boolean getAcceptUnknownOids() {
        return acceptUnknownOids;
    }

    //
    // private and protected variables
    //

    protected X500NameAttrMap attrMap;
    protected boolean acceptUnknownOids;

    //
    // private and protected static variables & methods.
    //

    protected static final String specialChars = ",+=<>#;";

    protected static final String valueEndChars = "+,;>";
    protected static final String quotedEndChars = "\"";
    protected static final String octoEndChars = " " + valueEndChars;

    /*
     * Values that aren't printable strings are emitted as BER-encoded
     * hex data.
     */
    protected static final String hexDigits = "0123456789ABCDEFabcdef";

    /**
     * Parse a sequence of hex pairs, each pair a UTF8 byte to a java string.
     * For example, "4C75C48D" is "Luc", the last c with caron.
     */
    protected static char[] getStringFromHexpairs(char[] hexPairs) throws UnsupportedEncodingException {
        try {
            byte[] buffer = new byte[hexPairs.length / 2];

            for (int i = 0; i < buffer.length; i++) {
                buffer[i] = (byte)
                        ((Character.digit(hexPairs[i * 2], 16) << 4) +
                        Character.digit(hexPairs[i * 2 + 1], 16));
            }

            Charset charset = Charset.forName("UTF-8");
            CharsetDecoder decoder = charset.newDecoder();

            CharBuffer charBuffer = decoder.decode(ByteBuffer.wrap(buffer));

            return Arrays.copyOfRange(charBuffer.array(),
                    charBuffer.arrayOffset(), charBuffer.arrayOffset() + charBuffer.limit());

        } catch (UnsupportedCharsetException e) {
            throw new UnsupportedEncodingException(
                    "No UTF8 byte to char converter to use for " +
                            "parsing LDAP DN String");

        } catch (CharacterCodingException e) {
            throw new IllegalArgumentException(
                    "Invalid hex pair in LDAP DN String.");
        }
    }
}
