io.micronaut.http.netty.body.JsonCounter Maven / Gradle / Ivy
 The newest version!
        
        /*
 * Copyright 2017-2023 original authors
 *
 * Licensed 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
 *
 * https://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 io.micronaut.http.netty.body;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.json.JsonSyntaxException;
import io.netty.buffer.ByteBuf;
/**
 * This class takes in JSON data and does simple parsing to detect boundaries between json nodes.
 * For example, this class can recognize the separation between the two JSON objects in
 * {@code {"foo":"bar"} {"bar":"baz"}}.
 * Public for fuzzing.
 */
@SuppressWarnings({"BooleanMethodIsAlwaysInverted", "InnerAssignment"})
@Internal
public final class JsonCounter {
    /**
     * Total number of bytes consumed.
     */
    private long position;
    /**
     * Depth of nested structures.
     */
    private int depth;
    /**
     * Current state of the parser.
     */
    private State state = State.BASE;
    /**
     * {@link #position} of the first byte of the current top-level JSON node.
     */
    private long bufferStart = -1;
    /**
     * Whether we are currently unwrapping a top-level array.
     *
     * @see #unwrapTopLevelArray()
     */
    private boolean unwrappingArray;
    /**
     * Whether we are currently unwrapping a top-level array, and expect a comma next (or end of
     * array).
     *
     * @see #unwrapTopLevelArray()
     */
    private boolean allowUnwrappingArrayComma;
    /**
     * The region of the last complete top-level JSON node we have visited. Polled by the user.
     */
    @Nullable
    private BufferRegion lastFlushedRegion;
    /**
     * Parse some input data. If {@code buf} is readable, this method always advances (always
     * consumes at least one byte).
     *
     * @param buf The input buffer
     * @throws JsonSyntaxException If there is a syntax error in the JSON. Note that not all syntax
     *                             errors are detected by this class.
     */
    public void feed(ByteBuf buf) throws JsonSyntaxException {
        if (position < 4) {
            // RFC 4627 allows JSON to be encoded as UTF-8, UTF-16 or UTF-32. It also specifies a
            // charset detection algorithm using 0x00 bytes.
            // Later standards (RFC 8259) only permit UTF-8, but Jackson still allows other
            // charsets. To avoid potential parser differential vulnerabilities, we forbid any 0x00
            // bytes in the input. They never appear in valid UTF-8 JSON.
            // If the input is utf-16 or utf-32, one of the first four bytes will be 0. Checking
            // this separately and only for four bytes allows us to avoid the work in the hot loops
            // below.
            int r = buf.readableBytes();
            if ((r >= 1 && buf.getByte(0) == 0)
                || (r >= 2 && buf.getByte(1) == 0)
                || (r >= 3 && buf.getByte(2) == 0)
                || (r >= 4 && buf.getByte(3) == 0)) {
                throw new JsonSyntaxException("Input must be legal UTF-8 JSON");
            }
        }
        if (!isBuffering()) {
            proceedUntilBuffering(buf);
        }
        if (isBuffering()) {
            proceedUntilNonBuffering(buf);
        }
    }
    /**
     * Enable top-level array unwrapping: If the input starts with an array, that array's elements
     * are returned as individual JSON nodes, not the array all at once. 
     * Must be called before any data is processed, but can be called after
     * {@link #noTokenization()}.
     */
    public void unwrapTopLevelArray() {
        if (position != 0) {
            throw new IllegalStateException("Already consumed input");
        }
        state = State.BEFORE_UNWRAP_ARRAY;
        bufferStart = -1;
    }
    /**
     * Do not perform any tokenization, assume that there is only one root-level value. There is
     * still some basic validation (ensuring the input isn't utf-16 or utf-32).
     */
    public void noTokenization() {
        if (position != 0) {
            throw new IllegalStateException("Already consumed input");
        }
        state = State.BUFFER_ALL;
        bufferStart = 0;
    }
    /**
     * Proceed until {@link #isBuffering()} becomes false.
     */
    @SuppressWarnings("java:S3776")
    private void proceedUntilNonBuffering(ByteBuf buf) throws JsonSyntaxException {
        assert isBuffering();
        int end = buf.writerIndex();
        int i = buf.readerIndex();
        while (i < end && bufferStart != -1) {
            int start = i;
            if (state == State.BASE) {
                assert depth > 0 : depth;
                for (; i < end; i++) {
                    if (!skipBufferingBase(buf.getByte(i))) {
                        break;
                    }
                }
                this.position += i - start;
                if (i < end) {
                    handleBufferingBaseSpecial(buf.getByte(i));
                    i++;
                    position++;
                }
            } else if (state == State.STRING) {
                for (; i < end; i++) {
                    if (!skipString(buf.getByte(i))) {
                        break;
                    }
                }
                this.position += i - start;
                if (i < end) {
                    handleStringSpecial(buf.getByte(i));
                    i++;
                    position++;
                }
            } else if (state == State.ESCAPE) {
                handleEscape(buf.getByte(i));
                i++;
                position++;
            } else if (state == State.TOP_LEVEL_SCALAR) {
                assert depth == 0 : depth;
                for (; i < end; i++) {
                    if (!skipTopLevelScalar(buf.getByte(i))) {
                        break;
                    }
                }
                this.position += i - start;
                if (i < end) {
                    handleTopLevelScalarSpecial(buf.getByte(i));
                    i++;
                    position++;
                }
            } else if (state == State.BUFFER_ALL) {
                i = end;
                position += i - start;
            } else {
                throw new AssertionError(state);
            }
        }
        buf.readerIndex(i);
    }
    /**
     * Consume some input until {@link #isBuffering()}. Sometimes this method returns before that
     * is the case, to make the implementation simpler.
     */
    @SuppressWarnings("java:S3776")
    private void proceedUntilBuffering(ByteBuf buf) throws JsonSyntaxException {
        assert !isBuffering();
        assert depth == 0 : depth;
        int start = buf.readerIndex();
        int end = buf.writerIndex();
        int i = start;
        if (state == State.AFTER_UNWRAP_ARRAY) {
            // top-level array consumed. reject further data
            i = skipWs(buf, i, end);
            if (i < end) {
                throw new JsonSyntaxException("Superfluous data after top-level array in streaming mode");
            }
        } else {
            // normal path
            assert state == State.BASE || state == State.BEFORE_UNWRAP_ARRAY : state;
            if (position == 0 && i < end && buf.getByte(i) == (byte) 0xef) {
                throw new JsonSyntaxException("UTF-8 BOM not allowed");
            }
            // if we are unwrapping a top-level array, search for a comma
            if (allowUnwrappingArrayComma) {
                i = skipWs(buf, i, end);
                if (i < end && buf.getByte(i) == ',') {
                    allowUnwrappingArrayComma = false;
                    i++;
                }
            }
            i = skipWs(buf, i, end);
            this.position += i - start;
            if (i < end) {
                byte b = buf.getByte(i);
                handleNonBufferingBase(b);
                i++;
                position++;
            }
        }
        buf.readerIndex(i);
    }
    /**
     * Skip any whitespace characters.
     *
     * @param i   The start index
     * @param end The maximum index
     * @return The first non-whitespace character index, or {@code end}
     */
    private static int skipWs(ByteBuf buf, int i, int end) {
        for (; i < end; i++) {
            if (!ws(buf.getByte(i))) {
                break;
            }
        }
        return i;
    }
    /**
     * Handle a special byte (anything but whitespace) in the base state, while not buffering.
     */
    private void handleNonBufferingBase(byte b) throws JsonSyntaxException {
        switch (b) {
            case '}' -> failMismatchedBrackets();
            case ']' -> {
                if (unwrappingArray) {
                    state = State.AFTER_UNWRAP_ARRAY;
                } else {
                    failMismatchedBrackets();
                }
            }
            case '{' -> {
                depth = 1;
                bufferStart = position;
                state = State.BASE; // we might be in BEFORE_UNWRAP_ARRAY
            }
            case '[' -> {
                if (state == State.BEFORE_UNWRAP_ARRAY) {
                    state = State.BASE;
                    unwrappingArray = true;
                } else {
                    depth = 1;
                    bufferStart = position;
                }
            }
            case '"' -> {
                state = State.STRING;
                bufferStart = position;
            }
            default -> {
                state = State.TOP_LEVEL_SCALAR;
                bufferStart = position;
            }
        }
    }
    /**
     * @return {@code true} if this character does not end the top-level scalar
     */
    private static boolean skipTopLevelScalar(byte b) {
        return !ws(b) && b != '"' && b != '{' && b != '[' && b != ']' && b != '}' && b != ',';
    }
    /**
     * Handle a special byte (anything but {@link #skipTopLevelScalar}) in the
     * {@link State#TOP_LEVEL_SCALAR} state.
     */
    private void handleTopLevelScalarSpecial(byte b) throws JsonSyntaxException {
        if (ws(b)) {
            position--;
            flushAfter();
            position++;
            allowUnwrappingArrayComma = unwrappingArray;
            state = State.BASE;
        } else if (unwrappingArray && (b == ',' || b == ']')) {
            position--;
            flushAfter();
            position++;
            if (b == ',') {
                state = State.BASE;
            } else {
                state = State.AFTER_UNWRAP_ARRAY;
            }
            allowUnwrappingArrayComma = false;
        } else {
            failMissingWs();
        }
    }
    /**
     * Handle a byte in the {@link State#ESCAPE} state.
     */
    private void handleEscape(byte b) {
        state = State.STRING;
    }
    /**
     * @return {@code true} if this character does not end the string
     */
    private static boolean skipString(byte b) {
        return b != '"' && b != '\\';
    }
    /**
     * Handle a special byte (anything but {@link #skipString}) in the {@link State#STRING} state.
     */
    private void handleStringSpecial(byte b) throws JsonSyntaxException {
        switch (b) {
            case '"' -> {
                state = State.BASE;
                if (depth == 0) {
                    flushAfter();
                }
            }
            case '\\' -> state = State.ESCAPE;
            default -> throw new AssertionError();
        }
    }
    /**
     * @return {@code true} if this character does not change the state while in {@link State#BASE}
     * and while not buffering
     */
    @SuppressWarnings("java:S2178") // performance
    private static boolean skipBufferingBase(byte b) {
        return (b != '"') & (b != '{') & (b != '[') & (b != ']') & (b != '}');
    }
    /**
     * Handle a special byte (anything but {@link #skipBufferingBase(byte)}) in the base state,
     * while buffering.
     */
    private void handleBufferingBaseSpecial(byte b) throws JsonSyntaxException {
        switch (b) {
            case '}', ']' -> {
                depth--;
                if (depth == 0) {
                    flushAfter();
                }
            }
            case '{', '[' -> depth = Math.incrementExact(depth);
            case '"' -> state = State.STRING;
            default -> throw new AssertionError(b);
        }
    }
    /**
     * Flush the current JSON node, starting at {@link #bufferStart}, and ending after
     * {@link #position}. Disables buffering.
     */
    private void flushAfter() {
        if (lastFlushedRegion != null) {
            throw new IllegalStateException("Should have cleared last buffer region");
        }
        assert bufferStart != -1;
        assert position >= bufferStart;
        lastFlushedRegion = new BufferRegion(bufferStart, position + 1);
        bufferStart = -1;
        allowUnwrappingArrayComma = unwrappingArray;
    }
    /**
     * Check for any new flushed data from the last {@link #feed(ByteBuf)} operation.
     *
     * @return The region that contains a JSON node, relative to {@link #position()}, or
     * {@code null} if the JSON node has not completed yet.
     */
    @Nullable
    public BufferRegion pollFlushedRegion() {
        BufferRegion r = lastFlushedRegion;
        lastFlushedRegion = null;
        return r;
    }
    /**
     * The current position counter of the parser. Increases by exactly one for each byte consumed
     * by {@link #feed}.
     *
     * @return The current position
     */
    public long position() {
        return position;
    }
    /**
     * Whether we are currently in the buffering state, i.e. there is a JSON node, but it's not
     * done yet or we can't know for sure that it's done (e.g. for numbers). This is used to flush
     * any remaining buffering data when EOF is reached.
     *
     * @return {@code true} if we are currently buffering
     */
    public boolean isBuffering() {
        return bufferStart != -1;
    }
    /**
     * If we are {@link #isBuffering() buffering}, the start {@link #position()} of the region that
     * is being buffered.
     *
     * @return The buffer region start
     * @throws IllegalStateException if we aren't buffering
     */
    public long bufferStart() {
        if (bufferStart == -1) {
            throw new IllegalStateException("Not buffering");
        }
        return bufferStart;
    }
    private static void failMismatchedBrackets() throws JsonSyntaxException {
        throw new JsonSyntaxException("JSON has mismatched brackets");
    }
    private static void failMissingWs() throws JsonSyntaxException {
        // we *could* support this, but jackson doesn't, and this makes the
        // implementation a little easier (we can do with returning a boolean)
        throw new JsonSyntaxException("After top-level scalars, there must be whitespace before the next node");
    }
    private static boolean ws(byte b) {
        return b == ' ' || b == '\n' || b == '\r' || b == '\t';
    }
    private enum State {
        /**
         * Default state, anything that's not inside a string, not a top-level scalar (numbers,
         * booleans, null), and not a special state for {@link #unwrapTopLevelArray() unwrapping}.
         */
        BASE,
        /**
         * State inside a string. Braces are ignored, and escape sequences get special handling.
         */
        STRING,
        /**
         * State inside a "top-level scalar", i.e. a boolean, number or {@code null} that is not
         * part of an array or object. These are a bit special because unlike strings, which
         * terminate on {@code "}, and structures, which terminate on a bracket, these terminate on
         * whitespace.
         */
        TOP_LEVEL_SCALAR,
        /**
         * State just after {@code \} inside a {@link #STRING}. The next byte is ignored, and then
         * we return to {@link #STRING} state.
         */
        ESCAPE,
        /**
         * Special state for {@link #unwrapTopLevelArray() unwrapping}, before the top-level array.
         * At this point we don't know if there is a top-level array that we need to unwrap or not.
         */
        BEFORE_UNWRAP_ARRAY,
        /**
         * Special state for {@link #unwrapTopLevelArray() unwrapping}, after the closing brace of
         * a top-level array. Any further tokens after this are an error.
         */
        AFTER_UNWRAP_ARRAY,
        /**
         * Special state for {@link #noTokenization()}. The input is not visited at all, we just
         * assume everything is part of one root-level token and buffer it all.
         */
        BUFFER_ALL,
    }
    /**
     * A region that contains a JSON node. Positions are relative to {@link #position()}.
     *
     * @param start First byte position of this node
     * @param end   Position after the last byte of this node (i.e. it's exclusive)
     */
    public record BufferRegion(long start, long end) {
    }
}
    © 2015 - 2025 Weber Informatics LLC | Privacy Policy