RFC2253.java

// Copyright (c) ZeroC, Inc.

package com.zeroc.Ice.SSL;

import com.zeroc.Ice.ParseException;

import java.util.LinkedList;
import java.util.List;

//
// See RFC 2253 and RFC 1779.
//
class RFC2253 {
    static class RDNPair {
        String key;
        String value;
    }

    static class RDNEntry {
        List<RDNPair> rdn = new LinkedList<>();
        boolean negate;
    }

    private static class ParseState {
        String data;
        int pos;
    }

    public static List<RDNEntry> parse(String data) throws ParseException {
        List<RDNEntry> results = new LinkedList<>();
        RDNEntry current = new RDNEntry();
        ParseState state = new ParseState();
        state.data = data;
        state.pos = 0;
        while (state.pos < state.data.length()) {
            eatWhite(state);
            if (state.pos < state.data.length() && state.data.charAt(state.pos) == '!') {
                if (!current.rdn.isEmpty()) {
                    throw new ParseException("negation symbol '!' must appear at start of list");
                }
                ++state.pos;
                current.negate = true;
            }
            current.rdn.add(parseNameComponent(state));
            eatWhite(state);
            if (state.pos < state.data.length() && state.data.charAt(state.pos) == ',') {
                ++state.pos;
            } else if (state.pos < state.data.length() && state.data.charAt(state.pos) == ';') {
                ++state.pos;
                results.add(current);
                current = new RDNEntry();
            } else if (state.pos < state.data.length()) {
                throw new ParseException(
                    "expected ',' or ';' at `" + state.data.substring(state.pos) + "'");
            }
        }
        if (!current.rdn.isEmpty()) {
            results.add(current);
        }

        return results;
    }

    public static List<RDNPair> parseStrict(String data) throws ParseException {
        List<RDNPair> results = new LinkedList<>();
        ParseState state = new ParseState();
        state.data = data;
        state.pos = 0;
        while (state.pos < state.data.length()) {
            results.add(parseNameComponent(state));
            eatWhite(state);
            if (state.pos < state.data.length()
                && (state.data.charAt(state.pos) == ','
                || state.data.charAt(state.pos) == ';')) {
                ++state.pos;
            } else if (state.pos < state.data.length()) {
                throw new ParseException(
                    "expected ',' or ';' at `" + state.data.substring(state.pos) + "'");
            }
        }
        return results;
    }

    private static RDNPair parseNameComponent(ParseState state) throws ParseException {
        RDNPair result = parseAttributeTypeAndValue(state);
        while (state.pos < state.data.length()) {
            eatWhite(state);
            if (state.pos < state.data.length() && state.data.charAt(state.pos) == '+') {
                ++state.pos;
            } else {
                break;
            }
            RDNPair p = parseAttributeTypeAndValue(state);
            result.value += "+";
            result.value += p.key;
            result.value += '=';
            result.value += p.value;
        }
        return result;
    }

    private static RDNPair parseAttributeTypeAndValue(ParseState state) throws ParseException {
        RDNPair p = new RDNPair();
        p.key = parseAttributeType(state);
        eatWhite(state);
        if (state.pos >= state.data.length()) {
            throw new ParseException(
                "invalid attribute type/value pair (unexpected end of state.data)");
        }
        if (state.data.charAt(state.pos) != '=') {
            throw new ParseException("invalid attribute type/value pair (missing =)");
        }
        ++state.pos;
        p.value = parseAttributeValue(state);
        return p;
    }

    private static String parseAttributeType(ParseState state) throws ParseException {
        eatWhite(state);
        if (state.pos >= state.data.length()) {
            throw new ParseException("invalid attribute type (expected end of state.data)");
        }

        StringBuffer result = new StringBuffer();

        //
        // RFC 1779.
        // <key> ::= 1*( <keychar> ) | "OID." <oid> | "oid." <oid>
        // <oid> ::= <digitString> | <digitstring> "." <oid>
        // RFC 2253:
        // attributeType = (ALPHA 1*keychar) | oid
        // keychar    = ALPHA | DIGIT | "-"
        // oid        = 1*DIGIT *("." 1*DIGIT)
        //
        // In section 4 of RFC 2253 the document says:
        // Implementations MUST allow an oid in the attribute type to be
        // prefixed by one of the character Strings "oid." or "OID.".
        //
        // Here we must also check for "oid." and "OID." before parsing
        // according to the ALPHA KEYCHAR* rule.
        //
        // First the OID case.
        //
        if (Character.isDigit(state.data.charAt(state.pos))
            || (state.data.length() - state.pos >= 4
            && ("oid.".equals(state.data.substring(state.pos, state.pos + 4))
            || "OID."
            .equals(state.data
                .substring(state.pos, state.pos + 4))))) {
            if (!Character.isDigit(state.data.charAt(state.pos))) {
                result.append(state.data.substring(state.pos, state.pos + 4));
                state.pos += 4;
            }

            while (true) {
                // 1*DIGIT
                while (state.pos < state.data.length()
                    && Character.isDigit(state.data.charAt(state.pos))) {
                    result.append(state.data.charAt(state.pos));
                    ++state.pos;
                }
                // "." 1*DIGIT
                if (state.pos < state.data.length() && state.data.charAt(state.pos) == '.') {
                    result.append(state.data.charAt(state.pos));
                    ++state.pos;
                    // 1*DIGIT must follow "."
                    if (state.pos < state.data.length()
                        && !Character.isDigit(state.data.charAt(state.pos))) {
                        throw new ParseException(
                            "invalid attribute type (expected end of state.data)");
                    }
                } else {
                    break;
                }
            }
        } else if (Character.isUpperCase(state.data.charAt(state.pos))
            || Character.isLowerCase(state.data.charAt(state.pos))) {
            //
            // The grammar is wrong in this case. It should be ALPHA
            // KEYCHAR* otherwise it will not accept "O" as a valid
            // attribute type.
            //
            result.append(state.data.charAt(state.pos));
            ++state.pos;
            // 1* KEYCHAR
            while (state.pos < state.data.length()
                && (Character.isDigit(state.data.charAt(state.pos))
                || Character.isUpperCase(state.data.charAt(state.pos))
                || Character.isLowerCase(state.data.charAt(state.pos))
                || state.data.charAt(state.pos) == '-')) {
                result.append(state.data.charAt(state.pos));
                ++state.pos;
            }
        } else {
            throw new ParseException("invalid attribute type");
        }
        return result.toString();
    }

    private static String parseAttributeValue(ParseState state) throws ParseException {
        eatWhite(state);
        if (state.pos >= state.data.length()) {
            return "";
        }

        //
        // RFC 2253
        // # hexString
        //
        StringBuffer result = new StringBuffer();
        if (state.data.charAt(state.pos) == '#') {
            result.append(state.data.charAt(state.pos));
            ++state.pos;
            while (true) {
                String h = parseHexPair(state, true);
                if (h.isEmpty()) {
                    break;
                }
                result.append(h);
            }
        } else if (state.data.charAt(state.pos) == '"') {
            result.append(state.data.charAt(state.pos));
            ++state.pos;
            while (true) {
                if (state.pos >= state.data.length()) {
                    throw new ParseException(
                        "invalid attribute value (unexpected end of state.data)");
                }
                // final terminating "
                if (state.data.charAt(state.pos) == '"') {
                    result.append(state.data.charAt(state.pos));
                    ++state.pos;
                    break;
                } else if (state.data.charAt(state.pos) != '\\') {
                    result.append(state.data.charAt(state.pos));
                    ++state.pos;
                } else {
                    result.append(parsePair(state));
                }
            }
        } else {
            while (state.pos < state.data.length()) {
                if (state.data.charAt(state.pos) == '\\') {
                    result.append(parsePair(state));
                } else if (special.indexOf(state.data.charAt(state.pos)) == -1
                    && state.data.charAt(state.pos) != '"') {
                    result.append(state.data.charAt(state.pos));
                    ++state.pos;
                } else {
                    break;
                }
            }
        }
        return result.toString();
    }

    //
    // RFC2253:
    // pair       = "\" ( special | "\" | QUOTATION | hexpair )
    //
    private static String parsePair(ParseState state) throws ParseException {
        String result = "";

        assert (state.data.charAt(state.pos) == '\\');
        result += state.data.charAt(state.pos);
        ++state.pos;

        if (state.pos >= state.data.length()) {
            throw new ParseException("invalid escape format (unexpected end of state.data)");
        }

        if (special.indexOf(state.data.charAt(state.pos)) != -1
            || state.data.charAt(state.pos) != '\\'
            || state.data.charAt(state.pos) != '"') {
            result += state.data.charAt(state.pos);
            ++state.pos;
            return result;
        }
        return parseHexPair(state, false);
    }

    //
    // RFC 2253
    // hexpair    = hexchar hexchar
    //
    private static String parseHexPair(ParseState state, boolean allowEmpty) throws ParseException {
        String result = "";
        if (state.pos < state.data.length()
            && hexvalid.indexOf(state.data.charAt(state.pos)) != -1) {
            result += state.data.charAt(state.pos);
            ++state.pos;
        }
        if (state.pos < state.data.length()
            && hexvalid.indexOf(state.data.charAt(state.pos)) != -1) {
            result += state.data.charAt(state.pos);
            ++state.pos;
        }
        if (result.length() != 2) {
            if (allowEmpty && result.isEmpty()) {
                return result;
            }
            throw new ParseException("invalid hex format");
        }
        return result;
    }

    //
    // RFC 2253:
    //
    // Implementations MUST allow for space (' ' ASCII 32) characters to be
    // present between name-component and ',', between attributeTypeAndValue
    // and '+', between attributeType and '=', and between '=' and
    // attributeValue.  These space characters are ignored when parsing.
    //
    private static void eatWhite(ParseState state) {
        while (state.pos < state.data.length() && state.data.charAt(state.pos) == ' ') {
            ++state.pos;
        }
    }

    private static final String special = ",=+<>#;";
    private static final String hexvalid = "0123456789abcdefABCDEF";

    private RFC2253() {}
}