StructuredField.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.tomcat.util.http.parser;

import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.tomcat.util.res.StringManager;

/**
 * Parsing of structured fields as per RFC 8941.
 * <p>
 * The parsing implementation is complete but not all elements are currently exposed via getters. Additional getters
 * will be added as required as the use of structured fields expands.
 * <p>
 * The serialization of structured fields has not been implemented.
 */
public class StructuredField {

    private static final StringManager sm = StringManager.getManager(StructuredField.class);

    private static final int ARRAY_SIZE = 128;

    private static final boolean[] IS_KEY_FIRST = new boolean[ARRAY_SIZE];
    private static final boolean[] IS_KEY = new boolean[ARRAY_SIZE];
    private static final boolean[] IS_OWS = new boolean[ARRAY_SIZE];
    private static final boolean[] IS_BASE64 = new boolean[ARRAY_SIZE];
    private static final boolean[] IS_TOKEN = new boolean[ARRAY_SIZE];

    static {
        for (int i = 0; i < ARRAY_SIZE; i++) {
            if (i == '*' || i >= 'a' && i <= 'z') {
                IS_KEY_FIRST[i] = true;
                IS_KEY[i] = true;
            } else if (i >= '0' && i <= '9' || i == '_' || i == '-' || i == '.') {
                IS_KEY[i] = true;
            }
        }

        for (int i = 0; i < ARRAY_SIZE; i++) {
            if (i == 9 || i == ' ') {
                IS_OWS[i] = true;
            }
        }

        for (int i = 0; i < ARRAY_SIZE; i++) {
            if (i == '+' || i == '/' || i >= '0' && i <= '9' || i == '=' || i >= 'A' && i <= 'Z' ||
                    i >= 'a' && i <= 'z') {
                IS_BASE64[i] = true;
            }
        }

        for (int i = 0; i < ARRAY_SIZE; i++) {
            if (HttpParser.isToken(i) || i == ':' || i == '/') {
                IS_TOKEN[i] = true;
            }
        }
    }


    static SfList parseSfList(Reader input) throws IOException {
        skipSP(input);

        SfList result = new SfList();

        if (peek(input) != -1) {
            while (true) {
                SfListMember listMember = parseSfListMember(input);
                result.addListMember(listMember);
                skipOWS(input);
                if (peek(input) == -1) {
                    break;
                }
                requireChar(input, ',');
                skipOWS(input);
                requireNotChar(input, -1);
            }
        }

        skipSP(input);
        requireChar(input, -1);
        return result;
    }


    // Item or inner list
    static SfListMember parseSfListMember(Reader input) throws IOException {
        SfListMember listMember;
        if (peek(input) == '(') {
            listMember = parseSfInnerList(input);
        } else {
            listMember = parseSfBareItem(input);
        }
        parseSfParameters(input, listMember);
        return listMember;
    }


    static SfInnerList parseSfInnerList(Reader input) throws IOException {
        requireChar(input, '(');

        SfInnerList innerList = new SfInnerList();

        while (true) {
            skipSP(input);
            if (peek(input) == ')') {
                break;
            }
            SfItem<?> item = parseSfBareItem(input);
            parseSfParameters(input, item);
            innerList.addListItem(item);
            input.mark(1);
            requireChar(input, ' ', ')');
            input.reset();
        }
        requireChar(input, ')');

        return innerList;
    }


    static SfDictionary parseSfDictionary(Reader input) throws IOException {
        skipSP(input);

        SfDictionary result = new SfDictionary();

        if (peek(input) != -1) {
            while (true) {
                String key = parseSfKey(input);
                SfListMember listMember;
                input.mark(1);
                int c = input.read();
                if (c == '=') {
                    listMember = parseSfListMember(input);
                } else {
                    listMember = new SfBoolean(true);
                    input.reset();
                }
                parseSfParameters(input, listMember);
                result.addDictionaryMember(key, listMember);
                skipOWS(input);
                if (peek(input) == -1) {
                    break;
                }
                requireChar(input, ',');
                skipOWS(input);
                requireNotChar(input, -1);
            }
        }

        skipSP(input);
        requireChar(input, -1);
        return result;
    }


    static SfItem<?> parseSfItem(Reader input) throws IOException {
        skipSP(input);

        SfItem<?> item = parseSfBareItem(input);
        parseSfParameters(input, item);

        skipSP(input);
        requireChar(input, -1);
        return item;
    }


    static SfItem<?> parseSfBareItem(Reader input) throws IOException {
        int c = input.read();

        SfItem<?> item;
        if (c == '-' || HttpParser.isNumeric(c)) {
            item = parseSfNumeric(input, c);
        } else if (c == '\"') {
            item = parseSfString(input);
        } else if (c == '*' || HttpParser.isAlpha(c)) {
            item = parseSfToken(input, c);
        } else if (c == ':') {
            item = parseSfByteSequence(input);
        } else if (c == '?') {
            item = parseSfBoolean(input);
        } else {
            throw new IllegalArgumentException(
                    sm.getString("sf.bareitem.invalidCharacter", String.format("\\u%40X", Integer.valueOf(c))));
        }

        return item;
    }


    static void parseSfParameters(Reader input, SfListMember listMember) throws IOException {
        while (true) {
            if (peek(input) != ';') {
                break;
            }
            requireChar(input, ';');
            skipSP(input);
            String key = parseSfKey(input);
            SfItem<?> item;
            input.mark(1);
            int c = input.read();
            if (c == '=') {
                item = parseSfBareItem(input);
            } else {
                item = new SfBoolean(true);
                input.reset();
            }
            listMember.addParameter(key, item);
        }
    }


    static String parseSfKey(Reader input) throws IOException {
        StringBuilder result = new StringBuilder();

        input.mark(1);
        int c = input.read();
        if (!isKeyFirst(c)) {
            throw new IllegalArgumentException(
                    sm.getString("sf.key.invalidFirstCharacter", String.format("\\u%40X", Integer.valueOf(c))));
        }

        while (c != -1 && isKey(c)) {
            result.append((char) c);
            input.mark(1);
            c = input.read();
        }
        input.reset();
        return result.toString();
    }


    static SfItem<?> parseSfNumeric(Reader input, int first) throws IOException {
        int sign = 1;
        boolean integer = true;
        int decimalPos = 0;

        StringBuilder result = new StringBuilder();

        int c;
        if (first == '-') {
            sign = -1;
            c = input.read();
        } else {
            c = first;
        }

        if (!HttpParser.isNumeric(c)) {
            throw new IllegalArgumentException(
                    sm.getString("sf.numeric.invalidCharacter", String.format("\\u%40X", Integer.valueOf(c))));
        }
        result.append((char) c);
        input.mark(1);
        c = input.read();

        while (c != -1) {
            if (HttpParser.isNumeric(c)) {
                result.append((char) c);
            } else if (integer && c == '.') {
                if (result.length() > 12) {
                    throw new IllegalArgumentException(sm.getString("sf.numeric.integralPartTooLong"));
                }
                integer = false;
                result.append((char) c);
                decimalPos = result.length();
            } else {
                input.reset();
                break;
            }
            if (integer && result.length() > 15) {
                throw new IllegalArgumentException(sm.getString("sf.numeric.integerTooLong"));
            }
            if (!integer && result.length() > 16) {
                throw new IllegalArgumentException(sm.getString("sf.numeric.decimalTooLong"));
            }
            input.mark(1);
            c = input.read();
        }

        if (integer) {
            return new SfInteger(Long.parseLong(result.toString()) * sign);
        }

        if (result.charAt(result.length() - 1) == '.') {
            throw new IllegalArgumentException(sm.getString("sf.numeric.decimalInvalidFinal"));
        }

        if (result.length() - decimalPos > 3) {
            throw new IllegalArgumentException(sm.getString("sf.numeric.decimalPartTooLong"));
        }

        return new SfDecimal(Double.parseDouble(result.toString()) * sign);
    }


    static SfString parseSfString(Reader input) throws IOException {
        // It is known first character was '"'
        StringBuilder result = new StringBuilder();

        while (true) {
            int c = input.read();
            if (c == '\\') {
                requireNotChar(input, -1);
                c = input.read();
                if (c != '\\' && c != '\"') {
                    throw new IllegalArgumentException(
                            sm.getString("sf.string.invalidEscape", String.format("\\u%40X", Integer.valueOf(c))));
                }
            } else {
                if (c == '\"') {
                    break;
                }
                // This test also covers unexpected EOF
                if (c < 32 || c > 126) {
                    throw new IllegalArgumentException(
                            sm.getString("sf.string.invalidCharacter", String.format("\\u%40X", Integer.valueOf(c))));
                }
            }
            result.append((char) c);
        }

        return new SfString(result.toString());
    }


    static SfToken parseSfToken(Reader input, int first) throws IOException {
        // It is known first character is valid
        StringBuilder result = new StringBuilder();

        result.append((char) first);
        while (true) {
            input.mark(1);
            int c = input.read();
            if (!isToken(c)) {
                input.reset();
                break;
            }
            result.append((char) c);
        }

        return new SfToken(result.toString());
    }


    static SfByteSequence parseSfByteSequence(Reader input) throws IOException {
        // It is known first character was ':'
        StringBuilder base64 = new StringBuilder();

        while (true) {
            int c = input.read();

            if (c == ':') {
                break;
            } else if (isBase64(c)) {
                base64.append((char) c);
            } else {
                throw new IllegalArgumentException(
                        sm.getString("sf.base64.invalidCharacter", String.format("\\u%40X", Integer.valueOf(c))));
            }
        }

        return new SfByteSequence(Base64.getDecoder().decode(base64.toString()));
    }


    static SfBoolean parseSfBoolean(Reader input) throws IOException {
        // It is known first character was '?'
        int c = input.read();

        if (c == '1') {
            return new SfBoolean(true);
        } else if (c == '0') {
            return new SfBoolean(false);
        } else {
            throw new IllegalArgumentException(
                    sm.getString("sf.boolean.invalidCharacter", String.format("\\u%40X", Integer.valueOf(c))));
        }
    }


    static void skipSP(Reader input) throws IOException {
        input.mark(1);
        int c = input.read();
        while (c == 32) {
            input.mark(1);
            c = input.read();
        }
        input.reset();
    }


    static void skipOWS(Reader input) throws IOException {
        input.mark(1);
        int c = input.read();
        while (isOws(c)) {
            input.mark(1);
            c = input.read();
        }
        input.reset();
    }


    static void requireChar(Reader input, int... required) throws IOException {
        int c = input.read();
        for (int r : required) {
            if (c == r) {
                return;
            }
        }
        throw new IllegalArgumentException(
                sm.getString("sf.invalidCharacter", String.format("\\u%40X", Integer.valueOf(c))));
    }


    static void requireNotChar(Reader input, int required) throws IOException {
        input.mark(1);
        int c = input.read();
        if (c == required) {
            throw new IllegalArgumentException(
                    sm.getString("sf.invalidCharacter", String.format("\\u%40X", Integer.valueOf(c))));
        }
        input.reset();
    }


    static int peek(Reader input) throws IOException {
        input.mark(1);
        int c = input.read();
        input.reset();
        return c;
    }


    static boolean isKeyFirst(int c) {
        try {
            return IS_KEY_FIRST[c];
        } catch (ArrayIndexOutOfBoundsException ex) {
            return false;
        }
    }


    static boolean isKey(int c) {
        try {
            return IS_KEY[c];
        } catch (ArrayIndexOutOfBoundsException ex) {
            return false;
        }
    }


    static boolean isOws(int c) {
        try {
            return IS_OWS[c];
        } catch (ArrayIndexOutOfBoundsException ex) {
            return false;
        }
    }


    static boolean isBase64(int c) {
        try {
            return IS_BASE64[c];
        } catch (ArrayIndexOutOfBoundsException ex) {
            return false;
        }
    }


    static boolean isToken(int c) {
        try {
            return IS_TOKEN[c];
        } catch (ArrayIndexOutOfBoundsException ex) {
            return false;
        }
    }


    private StructuredField() {
        // Utility class. Hide default constructor.
    }


    static class SfDictionary {
        private Map<String,SfListMember> dictionary = new LinkedHashMap<>();

        void addDictionaryMember(String key, SfListMember value) {
            dictionary.put(key, value);
        }

        SfListMember getDictionaryMember(String key) {
            return dictionary.get(key);
        }
    }

    static class SfList {
        private List<SfListMember> listMembers = new ArrayList<>();

        void addListMember(SfListMember listMember) {
            listMembers.add(listMember);
        }
    }

    static class SfListMember {
        private Map<String,SfItem<?>> parameters = null;

        void addParameter(String key, SfItem<?> value) {
            if (parameters == null) {
                parameters = new LinkedHashMap<>();
            }
            parameters.put(key, value);
        }
    }

    static class SfInnerList extends SfListMember {
        List<SfItem<?>> listItems = new ArrayList<>();

        SfInnerList() {
            // Default constructor is NO-OP
        }

        void addListItem(SfItem<?> item) {
            listItems.add(item);
        }

        List<SfItem<?>> getListItem() {
            return listItems;
        }
    }

    abstract static class SfItem<T> extends SfListMember {
        private final T value;

        SfItem(T value) {
            this.value = value;
        }

        T getVaue() {
            return value;
        }
    }

    static class SfInteger extends SfItem<Long> {
        SfInteger(long value) {
            super(Long.valueOf(value));
        }
    }

    static class SfDecimal extends SfItem<Double> {
        SfDecimal(double value) {
            super(Double.valueOf(value));
        }
    }

    static class SfString extends SfItem<String> {
        SfString(String value) {
            super(value);
        }
    }

    static class SfToken extends SfItem<String> {
        SfToken(String value) {
            super(value);
        }
    }

    static class SfByteSequence extends SfItem<byte[]> {
        SfByteSequence(byte[] value) {
            super(value);
        }
    }

    static class SfBoolean extends SfItem<Boolean> {
        SfBoolean(boolean value) {
            super(Boolean.valueOf(value));
        }
    }
}