ParserBase.java

package tools.jackson.core.base;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;

import tools.jackson.core.*;
import tools.jackson.core.exc.InputCoercionException;
import tools.jackson.core.exc.JacksonIOException;
import tools.jackson.core.exc.StreamConstraintsException;
import tools.jackson.core.exc.StreamReadException;
import tools.jackson.core.io.ContentReference;
import tools.jackson.core.io.IOContext;
import tools.jackson.core.io.NumberInput;
import tools.jackson.core.util.ByteArrayBuilder;
import tools.jackson.core.util.TextBuffer;

/**
 * Intermediate base class used by many (but not all) Jackson {@link JsonParser}
 * implementations. Contains most common things that are independent
 * of actual underlying input source.
 */
public abstract class ParserBase extends ParserMinimalBase
{
    /*
    /**********************************************************************
    /* Current input data offsets
    /**********************************************************************
     */

    // Note: type of actual buffer depends on sub-class, can't include

    /**
     * Pointer to next available character in buffer
     */
    protected int _inputPtr;

    /**
     * Index of character after last available one in the buffer.
     */
    protected int _inputEnd;

    /*
    /**********************************************************************
    /* Current input location information
    /**********************************************************************
     */

    /**
     * Number of characters/bytes that were contained in previous blocks
     * (blocks that were already processed prior to the current buffer).
     */
    protected long _currInputProcessed;

    /**
     * Current row location of current point in input buffer, starting
     * from 1, if available.
     */
    protected int _currInputRow = 1;

    /**
     * Current index of the first character of the current row in input
     * buffer. Needed to calculate column position, if necessary; benefit
     * of not having column itself is that this only has to be updated
     * once per line.
     */
    protected int _currInputRowStart;

    /*
    /**********************************************************************
    /* Information about starting location of event
    /* Reader is pointing to; updated on-demand
    /**********************************************************************
     */

    // // // Location info at point when current token was started

    /**
     * Total number of bytes/characters read before start of current token.
     * For big (gigabyte-sized) sizes are possible, needs to be long,
     * unlike pointers and sizes related to in-memory buffers.
     */
    protected long _tokenInputTotal;

    /**
     * Input row on which current token starts, 1-based
     */
    protected int _tokenInputRow = 1;

    /**
     * Column on input row that current token starts; 0-based (although
     * in the end it'll be converted to 1-based)
     */
    protected int _tokenInputCol;

    /*
    /**********************************************************************
    /* Buffer(s) for local name(s) and text content
    /**********************************************************************
     */

    /**
     * Buffer that contains contents of String values, including
     * property names if necessary (name split across boundary,
     * contains escape sequence, or access needed to char array)
     */
    protected final TextBuffer _textBuffer;

    /**
     * ByteArrayBuilder is needed if 'getBinaryValue' is called. If so,
     * we better reuse it for remainder of content.
     */
    protected ByteArrayBuilder _byteArrayBuilder;

    /**
     * We will hold on to decoded binary data, for duration of
     * current event, so that multiple calls to
     * {@link #getBinaryValue} will not need to decode data more
     * than once.
     */
    protected byte[] _binaryValue;

    /*
    /**********************************************************************
    /* Numeric value state; multiple fields used for efficiency
    /**********************************************************************
     */

    /**
     * Bitfield that indicates which numeric representations
     * have been calculated for the current type
     */
    protected int _numTypesValid = NR_UNKNOWN;

    // First primitives

    protected int _numberInt;

    protected long _numberLong;

    protected float _numberFloat;

    protected double _numberDouble;

    // And then object types

    protected BigInteger _numberBigInt;

    protected BigDecimal _numberBigDecimal;

    /**
     * Textual number representation captured from input in cases lazy-parsing
     * is desired.
     */
    protected String _numberString;

    /**
     * Marker for explicit "Not a Number" (NaN) values that may be read
     * by some formats: this includes positive and negative infinity,
     * as well as "NaN" result for some arithmetic operations.
     *<p>
     * In case of JSON, such values can only be handled with non-standard
     * processing: for some other formats they can be passed normally.
     *<p>
     * NOTE: this marker is NOT set in case of value overflow/underflow for
     * {@code double} or {@code float} values.
     *
     * @since 2.17
     */
    protected boolean _numberIsNaN;

    // And then other information about value itself

    /**
     * Flag that indicates whether numeric value has a negative
     * value. That is, whether its textual representation starts
     * with minus character.
     */
    protected boolean _numberNegative;

    /**
     * Length of integer part of the number, in characters
     */
    protected int _intLength;

    /**
     * Length of the fractional part (not including decimal
     * point or exponent), in characters.
     * Not used for  pure integer values.
     */
    protected int _fractLength;

    /**
     * Length of the exponent part of the number, if any, not
     * including 'e' marker or sign, just digits.
     * Not used for  pure integer values.
     */
    protected int _expLength;

    /*
    /**********************************************************************
    /* Life-cycle
    /**********************************************************************
     */

    protected ParserBase(ObjectReadContext readCtxt,
            IOContext ctxt, int streamReadFeatures) {
        super(readCtxt, ctxt, streamReadFeatures);
        _textBuffer = ctxt.constructReadConstrainedTextBuffer();
    }

    /*
    /**********************************************************************
    /* Overrides for Feature handling
    /**********************************************************************
     */

    /*
    @Override
    public JsonParser enable(StreamReadFeature f) {
        _streamReadFeatures |= f.getMask();
        if (f == StreamReadFeature.STRICT_DUPLICATE_DETECTION) { // enabling dup detection?
            if (_parsingContext.getDupDetector() == null) { // but only if disabled currently
                _parsingContext = _parsingContext.withDupDetector(DupDetector.rootDetector(this));
            }
        }
        return this;
    }

    @Override
    public JsonParser disable(StreamReadFeature f) {
        _streamReadFeatures &= ~f.getMask();
        if (f == StreamReadFeature.STRICT_DUPLICATE_DETECTION) {
            _parsingContext = _parsingContext.withDupDetector(null);
        }
        return this;
    }
    */

    /*
    /**********************************************************************
    /* JsonParser impl
    /**********************************************************************
     */

    @Override
    public void assignCurrentValue(Object v) {
        TokenStreamContext ctxt = streamReadContext();
        if (ctxt != null) {
            ctxt.assignCurrentValue(v);
        }
    }

    @Override
    public Object currentValue() {
        TokenStreamContext ctxt = streamReadContext();
        return (ctxt == null) ? null : ctxt.currentValue();
    }

    /**
     * Method that can be called to get the name associated with
     * the current event.
     */
    /*
    @Override public String currentName() {
        // [JACKSON-395]: start markers require information from parent
        if (_currToken == JsonToken.START_OBJECT || _currToken == JsonToken.START_ARRAY) {
            JsonReadContext parent = _parsingContext.getParent();
            if (parent != null) {
                return parent.currentName();
            }
        }
        return _parsingContext.currentName();
    }
    */

    @Override
    public void close() throws JacksonException
    {
        super.close();
        // 19-Jan-2018, tatu: as per [core#440] need to ensure no more data assumed available
        _inputPtr = Math.max(_inputPtr, _inputEnd);
    }

    // public TokenStreamLocation currentTokenLocation()
    // public TokenStreamLocation currentLocation()

    /*
    /**********************************************************************
    /* Public API, access to token information, text and similar
    /**********************************************************************
     */

    @Override
    public boolean hasStringCharacters() {
        return false;
    }

    @SuppressWarnings("resource")
    @Override
    public byte[] getBinaryValue(Base64Variant variant) throws JacksonException
    {
        if (_binaryValue == null) {
            if (_currToken != JsonToken.VALUE_STRING) {
                _reportError("Current token (%s) not VALUE_EMBEDDED_OBJECT or VALUE_STRING, cannot access as binary", _currToken);
            }
            ByteArrayBuilder builder = _getByteArrayBuilder();
            _decodeBase64(getString(), builder, variant);
            _binaryValue = builder.toByteArray();
        }
        return _binaryValue;
    }

    /*
    /**********************************************************************
    /* Public low-level accessors
    /**********************************************************************
     */

    public long getTokenCharacterOffset() { return _tokenInputTotal; }
    public int getTokenLineNr() { return _tokenInputRow; }
    public int getTokenColumnNr() {
        // note: value of -1 means "not available"; otherwise convert from 0-based to 1-based
        int col = _tokenInputCol;
        return (col < 0) ? col : (col + 1);
    }

    /*
    /**********************************************************************
    /* Low-level reading, other
    /**********************************************************************
     */

    @Override
    protected void _releaseBuffers() {
        _textBuffer.releaseBuffers();
    }

    /**
     * Method called when an EOF is encountered between tokens.
     * If so, it may be a legitimate EOF, but <b>only</b> if there
     * is no open non-root context.
     */
    @Override
    protected void _handleEOF() throws JacksonException {
        TokenStreamContext parsingContext = streamReadContext();
        if ((parsingContext != null) && !parsingContext.inRoot()) {
            String marker = parsingContext.inArray() ? "Array" : "Object";
            _reportInvalidEOF(String.format(
                    ": expected close marker for %s (start marker at %s)",
                    marker,
                    parsingContext.startLocation(_contentReference())),
                    null);
        }
    }

    /**
     * @return If no exception is thrown, {@code -1} which is used as marked for "end-of-input"
     *
     * @throws StreamReadException If check on {@code _handleEOF()} fails; usually because
     *    the current context is not root context (missing end markers in content)
     */
    protected final int _eofAsNextChar() throws StreamReadException {
        _handleEOF();
        return -1;
    }

    /*
    /**********************************************************************
    /* Internal/package methods: shared/reusable builders
    /**********************************************************************
     */

    protected ByteArrayBuilder _getByteArrayBuilder()
    {
        if (_byteArrayBuilder == null) {
            _byteArrayBuilder = new ByteArrayBuilder();
        } else {
            _byteArrayBuilder.reset();
        }
        return _byteArrayBuilder;
    }

    /*
    /**********************************************************************
    /* Methods related to number handling
    /**********************************************************************
     */

    // // // Life-cycle of number-parsing

    protected final JsonToken reset(boolean negative, int intLen, int fractLen, int expLen)
        throws JacksonException
    {
        if (fractLen < 1 && expLen < 1) { // integer
            return resetInt(negative, intLen);
        }
        return resetFloat(negative, intLen, fractLen, expLen);
    }

    protected final JsonToken resetInt(boolean negative, int intLen)
        throws JacksonException
    {
        // May throw StreamConstraintsException:
        _streamReadConstraints.validateIntegerLength(intLen);
        _numberNegative = negative;
        _numberIsNaN = false;
        _intLength = intLen;
        _fractLength = 0;
        _expLength = 0;
        _numTypesValid = NR_UNKNOWN; // to force decoding
        _numberString = null;
        return JsonToken.VALUE_NUMBER_INT;
    }

    protected final JsonToken resetFloat(boolean negative, int intLen, int fractLen, int expLen)
        throws JacksonException
    {
        // May throw StreamConstraintsException:
        _streamReadConstraints.validateFPLength(intLen + fractLen + expLen);
        _numberNegative = negative;
        _numberIsNaN = false;
        _intLength = intLen;
        _fractLength = fractLen;
        _expLength = expLen;
        _numTypesValid = NR_UNKNOWN; // to force decoding
        _numberString = null;
        return JsonToken.VALUE_NUMBER_FLOAT;
    }

    protected final JsonToken resetAsNaN(String valueStr, double value)
    {
        _textBuffer.resetWithString(valueStr);
        _numberDouble = value;
        _numTypesValid = NR_DOUBLE;
        _numberIsNaN = true;
        _numberString = null;
        return JsonToken.VALUE_NUMBER_FLOAT;
    }

    @Override
    public boolean isNaN() {
        // 01-Dec-2023, tatu: [core#1137] Only return explicit NaN
        return (_currToken == JsonToken.VALUE_NUMBER_FLOAT)
                && _numberIsNaN;
    }

    /*
    /**********************************************************************
    /* Numeric accessors of public API
    /**********************************************************************
     */

    @Override
    public Number getNumberValue()
    {
        if (_numTypesValid == NR_UNKNOWN) {
            _parseNumericValue(NR_UNKNOWN); // will also check event type
        }
        // Separate types for int types
        if (_currToken == JsonToken.VALUE_NUMBER_INT) {
            if ((_numTypesValid & NR_INT) != 0) {
                return _numberInt;
            }
            if ((_numTypesValid & NR_LONG) != 0) {
                return _numberLong;
            }
            if ((_numTypesValid & NR_BIGINT) != 0) {
                return _getBigInteger();
            }
            _throwInternal();
        }

        // And then floating point types. But here optimal type
        // needs to be big decimal, to avoid losing any data?
        if ((_numTypesValid & NR_BIGDECIMAL) != 0) {
            return _getBigDecimal();
        }
        if ((_numTypesValid & NR_FLOAT) != 0) {
            return _getNumberFloat();
        }
        if ((_numTypesValid & NR_DOUBLE) == 0) { // sanity check
            _throwInternal();
        }
        return _getNumberDouble();
    }

    // NOTE: mostly copied from above
    @Override
    public Number getNumberValueExact()
    {
        if (_currToken == JsonToken.VALUE_NUMBER_INT) {
            if (_numTypesValid == NR_UNKNOWN) {
                _parseNumericValue(NR_UNKNOWN);
            }
            if ((_numTypesValid & NR_INT) != 0) {
                return _numberInt;
            }
            if ((_numTypesValid & NR_LONG) != 0) {
                return _numberLong;
            }
            if ((_numTypesValid & NR_BIGINT) != 0) {
                return _getBigInteger();
            }
            _throwInternal();
        }
        // 09-Jul-2020, tatu: [databind#2644] requires we will retain accuracy, so:
        if (_numTypesValid == NR_UNKNOWN) {
            _parseNumericValue(NR_BIGDECIMAL);
        }
        if ((_numTypesValid & NR_BIGDECIMAL) != 0) {
            return _getBigDecimal();
        }
        if ((_numTypesValid & NR_FLOAT) != 0) {
            return _getNumberFloat();
        }
        if ((_numTypesValid & NR_DOUBLE) == 0) { // sanity check
            _throwInternal();
        }
        return _getNumberDouble();
    }

    @Override
    public Object getNumberValueDeferred()
    {
        if (_currToken == JsonToken.VALUE_NUMBER_INT) {
            if (_numTypesValid == NR_UNKNOWN) {
                _parseNumericValue(NR_UNKNOWN);
            }
            if ((_numTypesValid & NR_INT) != 0) {
                return _numberInt;
            }
            if ((_numTypesValid & NR_LONG) != 0) {
                return _numberLong;
            }
            if ((_numTypesValid & NR_BIGINT) != 0) {
                // from _getBigInteger()
                if (_numberBigInt != null) {
                    return _numberBigInt;
                }
                if (_numberString != null) {
                    return _numberString;
                }
                return _getBigInteger(); // will fail
            }
            _throwInternal();
        }
        if (_currToken == JsonToken.VALUE_NUMBER_FLOAT) {
            // Ok this gets tricky since flags are not set quite as with
            // integers
            if ((_numTypesValid & NR_BIGDECIMAL) != 0) {
                return _getBigDecimal();
            }
            if ((_numTypesValid & NR_DOUBLE) != 0) { // sanity check
                return _getNumberDouble();
            }
            if ((_numTypesValid & NR_FLOAT) != 0) {
                return _getNumberFloat();
            }
            // Should be able to rely on this; might want to set _numberString
            // but state keeping looks complicated so don't do that yet
            return _textBuffer.contentsAsString();
        }
        // We'll just force exception by:
        return getNumberValue();
    }

    @Override
    public NumberType getNumberType()
    {
        if (_numTypesValid == NR_UNKNOWN) {
            // 29-May-2025, tatu: [core#1434] Short-circuit for non-numbers
            if (_currToken != JsonToken.VALUE_NUMBER_INT
                    && _currToken != JsonToken.VALUE_NUMBER_FLOAT) {
                 return null;
            }
            _parseNumericValue(NR_UNKNOWN); // will also check event type
        }
        if (_currToken == JsonToken.VALUE_NUMBER_INT) {
            if ((_numTypesValid & NR_INT) != 0) {
                return NumberType.INT;
            }
            if ((_numTypesValid & NR_LONG) != 0) {
                return NumberType.LONG;
            }
            return NumberType.BIG_INTEGER;
        }

        /* And then floating point types. Here optimal type
         * needs to be big decimal, to avoid losing any data?
         * However... using BD is slow, so let's allow returning
         * double as type if no explicit call has been made to access
         * data as BD?
         */
        if ((_numTypesValid & NR_BIGDECIMAL) != 0) {
            return NumberType.BIG_DECIMAL;
        }
        if ((_numTypesValid & NR_FLOAT) != 0) {
            return NumberType.FLOAT;
        }
        return NumberType.DOUBLE;
    }

    @Override
    public int getIntValue() throws JacksonException
    {
        if ((_numTypesValid & NR_INT) == 0) {
            if (_numTypesValid == NR_UNKNOWN) { // not parsed at all
                return _parseIntValue();
            }
            if ((_numTypesValid & NR_INT) == 0) { // wasn't an int natively?
                convertNumberToInt(); // let's make it so, if possible
            }
        }
        return _numberInt;
    }

    @Override
    public long getLongValue() throws JacksonException
    {
        if ((_numTypesValid & NR_LONG) == 0) {
            if (_numTypesValid == NR_UNKNOWN) {
                _parseNumericValue(NR_LONG);
            }
            if ((_numTypesValid & NR_LONG) == 0) {
                convertNumberToLong();
            }
        }
        return _numberLong;
    }

    @Override
    public BigInteger getBigIntegerValue() throws JacksonException
    {
        if ((_numTypesValid & NR_BIGINT) == 0) {
            if (_numTypesValid == NR_UNKNOWN) {
                _parseNumericValue(NR_BIGINT);
            }
            if ((_numTypesValid & NR_BIGINT) == 0) {
                convertNumberToBigInteger();
                return _numberBigInt;
            }
        }
        return _getBigInteger();
    }

    @Override
    public float getFloatValue() throws JacksonException
    {
        // 22-Jan-2009, tatu: Bounds/range checks would be tricky
        //   here, so let's not bother even trying...
        /*
        if (value < -Float.MAX_VALUE || value > MAX_FLOAT_D) {
            _reportError("Numeric value ("+getText()+") out of range of Java float");
        }
        */
        if ((_numTypesValid & NR_FLOAT) == 0) {
            if (_numTypesValid == NR_UNKNOWN) {
                _parseNumericValue(NR_FLOAT);
            }
            if ((_numTypesValid & NR_FLOAT) == 0) {
                convertNumberToFloat();
                return _numberFloat;
            }
        }
        return _getNumberFloat();
    }

    @Override
    public double getDoubleValue() throws JacksonException
    {
        if ((_numTypesValid & NR_DOUBLE) == 0) {
            if (_numTypesValid == NR_UNKNOWN) {
                _parseNumericValue(NR_DOUBLE);
            }
            if ((_numTypesValid & NR_DOUBLE) == 0) {
                convertNumberToDouble();
                return _numberDouble;
            }
        }
        return _getNumberDouble();
    }

    @Override
    public BigDecimal getDecimalValue() throws JacksonException
    {
        if ((_numTypesValid & NR_BIGDECIMAL) == 0) {
            if (_numTypesValid == NR_UNKNOWN) {
                _parseNumericValue(NR_BIGDECIMAL);
            }
            if ((_numTypesValid & NR_BIGDECIMAL) == 0) {
                convertNumberToBigDecimal();
                return _numberBigDecimal;
            }
        }
        return _getBigDecimal();
    }

    /*
    /**********************************************************************
    /* Abstract methods sub-classes will need to provide
    /**********************************************************************
     */

    /**
     * Method that will parse actual numeric value out of a syntactically
     * valid number value. Type it will parse into depends on whether
     * it is a floating point number, as well as its magnitude: smallest
     * legal type (of ones available) is used for efficiency.
     *
     * @param expType Numeric type that we will immediately need, if any;
     *   mostly necessary to optimize handling of floating point numbers
     *
     * @throws JacksonIOException for low-level read issues
     * @throws InputCoercionException if the current token not of numeric type
     * @throws tools.jackson.core.exc.StreamReadException for number decoding problems
     */
    protected abstract void _parseNumericValue(int expType)
        throws JacksonException, InputCoercionException;

    protected abstract int _parseIntValue() throws JacksonException;

    /*
    /**********************************************************************
    /* Numeric conversions
    /**********************************************************************
     */

    protected void convertNumberToInt() throws InputCoercionException
    {
        // First, converting from long ought to be easy
        if ((_numTypesValid & NR_LONG) != 0) {
            // Let's verify its lossless conversion by simple roundtrip
            int result = (int) _numberLong;
            if (result != _numberLong) {
                _reportOverflowInt(getString(), currentToken());
            }
            _numberInt = result;
        } else if ((_numTypesValid & NR_BIGINT) != 0) {
            final BigInteger bigInteger = _getBigInteger();
            if (BI_MIN_INT.compareTo(bigInteger) > 0
                    || BI_MAX_INT.compareTo(bigInteger) < 0) {
                _reportOverflowInt();
            }
            _numberInt = bigInteger.intValue();
        } else if ((_numTypesValid & NR_DOUBLE) != 0) {
            // Need to check boundaries
            final double d = _getNumberDouble();
            if (d < MIN_INT_D || d > MAX_INT_D) {
                _reportOverflowInt();
            }
            _numberInt = (int) d;
        } else if ((_numTypesValid & NR_BIGDECIMAL) != 0) {
            final BigDecimal bigDecimal = _getBigDecimal();
            if (BD_MIN_INT.compareTo(bigDecimal) > 0
                || BD_MAX_INT.compareTo(bigDecimal) < 0) {
                _reportOverflowInt();
            }
            _numberInt = bigDecimal.intValue();
        } else {
            _throwInternal();
        }
        _numTypesValid |= NR_INT;
    }

    protected void convertNumberToLong() throws InputCoercionException
    {
        if ((_numTypesValid & NR_INT) != 0) {
            _numberLong = _numberInt;
        } else if ((_numTypesValid & NR_BIGINT) != 0) {
            final BigInteger bigInteger = _getBigInteger();
            if (BI_MIN_LONG.compareTo(bigInteger) > 0
                    || BI_MAX_LONG.compareTo(bigInteger) < 0) {
                _reportOverflowLong();
            }
            _numberLong = bigInteger.longValue();
        } else if ((_numTypesValid & NR_DOUBLE) != 0) {
            // Need to check boundaries
            final double d = _getNumberDouble();
            if (d < MIN_LONG_D || d > MAX_LONG_D) {
                _reportOverflowLong();
            }
            _numberLong = (long) d;
        } else if ((_numTypesValid & NR_BIGDECIMAL) != 0) {
            final BigDecimal bigDecimal = _getBigDecimal();
            if (BD_MIN_LONG.compareTo(bigDecimal) > 0
                || BD_MAX_LONG.compareTo(bigDecimal) < 0) {
                _reportOverflowLong();
            }
            _numberLong = bigDecimal.longValue();
        } else {
            _throwInternal();
        }
        _numTypesValid |= NR_LONG;
    }

    protected void convertNumberToBigInteger()
    {
        if ((_numTypesValid & NR_BIGDECIMAL) != 0) {
            // here it'll just get truncated, no exceptions thrown
            _numberBigInt = _convertBigDecimalToBigInteger(_getBigDecimal());
        } else if ((_numTypesValid & NR_LONG) != 0) {
            _numberBigInt = BigInteger.valueOf(_numberLong);
        } else if ((_numTypesValid & NR_INT) != 0) {
            _numberBigInt = BigInteger.valueOf(_numberInt);
        } else if ((_numTypesValid & NR_DOUBLE) != 0) {
            if (_numberString != null) {
                _numberBigInt = _convertBigDecimalToBigInteger(_getBigDecimal());
            } else {
                _numberBigInt = _convertBigDecimalToBigInteger(BigDecimal.valueOf(_getNumberDouble()));
            }
        } else {
            _throwInternal();
        }
        _numTypesValid |= NR_BIGINT;
    }

    protected void convertNumberToDouble()
    {
        /* 05-Aug-2008, tatus: Important note: this MUST start with
         *   more accurate representations, since we don't know which
         *   value is the original one (others get generated when
         *   requested)
         */

        if ((_numTypesValid & NR_BIGDECIMAL) != 0) {
            if (_numberString != null) {
                _numberDouble = _getNumberDouble();
            } else {
                _numberDouble = _getBigDecimal().doubleValue();
            }
        } else if ((_numTypesValid & NR_BIGINT) != 0) {
            if (_numberString != null) {
                _numberDouble = _getNumberDouble();
            } else {
                _numberDouble = _getBigInteger().doubleValue();
            }
        } else if ((_numTypesValid & NR_LONG) != 0) {
            _numberDouble = _numberLong;
        } else if ((_numTypesValid & NR_INT) != 0) {
            _numberDouble = _numberInt;
        } else if ((_numTypesValid & NR_FLOAT) != 0) {
            if (_numberString != null) {
                _numberDouble = _getNumberDouble();
            } else {
                _numberDouble = _getNumberFloat();
            }
        } else {
            _throwInternal();
        }
        _numTypesValid |= NR_DOUBLE;
    }

    protected void convertNumberToFloat()
    {
        /* 05-Aug-2008, tatus: Important note: this MUST start with
         *   more accurate representations, since we don't know which
         *   value is the original one (others get generated when
         *   requested)
         */

        if ((_numTypesValid & NR_BIGDECIMAL) != 0) {
            if (_numberString != null) {
                _numberFloat = _getNumberFloat();
            } else {
                _numberFloat = _getBigDecimal().floatValue();
            }
        } else if ((_numTypesValid & NR_BIGINT) != 0) {
            if (_numberString != null) {
                _numberFloat = _getNumberFloat();
            } else {
                _numberFloat = _getBigInteger().floatValue();
            }
        } else if ((_numTypesValid & NR_LONG) != 0) {
            _numberFloat = _numberLong;
        } else if ((_numTypesValid & NR_INT) != 0) {
            _numberFloat = _numberInt;
        } else if ((_numTypesValid & NR_DOUBLE) != 0) {
            if (_numberString != null) {
                _numberFloat = _getNumberFloat();
            } else {
                _numberFloat = (float) _getNumberDouble();
            }
        } else {
            _throwInternal();
        }
        _numTypesValid |= NR_FLOAT;
    }

    protected void convertNumberToBigDecimal()
    {
        // 05-Aug-2008, tatus: Important note: this MUST start with more
        //   accurate representations, since we don't know which value is
        //   the original one (others get generated when requested)

        if ((_numTypesValid & NR_DOUBLE) != 0) {
            // Let's actually parse from String representation, to avoid
            // rounding errors that non-decimal floating operations could incur
            final String numStr = _numberString == null ? getString() : _numberString;
            _numberBigDecimal = NumberInput.parseBigDecimal(
                    numStr,
                    isEnabled(StreamReadFeature.USE_FAST_BIG_NUMBER_PARSER));
        } else if ((_numTypesValid & NR_BIGINT) != 0) {
            _numberBigDecimal = new BigDecimal(_getBigInteger());
        } else if ((_numTypesValid & NR_LONG) != 0) {
            _numberBigDecimal = BigDecimal.valueOf(_numberLong);
        } else if ((_numTypesValid & NR_INT) != 0) {
            _numberBigDecimal = BigDecimal.valueOf(_numberInt);
        } else {
            _throwInternal();
        }
        _numTypesValid |= NR_BIGDECIMAL;
    }

    // @since 2.15
    protected BigInteger _convertBigDecimalToBigInteger(BigDecimal bigDec) {
        // 04-Apr-2022, tatu: wrt [core#968] Need to limit max scale magnitude
        //   (may throw StreamConstraintsException)
        _streamReadConstraints.validateBigIntegerScale(bigDec.scale());
        return bigDec.toBigInteger();
    }

    /**
     * Internal accessor that needs to be used for accessing number value of type
     * {@link BigInteger} which -- as of 2.14 -- is typically lazily parsed.
     *
     * @return BigInteger value decoded or converted for the current event
     */
    protected BigInteger _getBigInteger() {
        if (_numberBigInt != null) {
            return _numberBigInt;
        }
        if (_numberString == null) {
            throw new IllegalStateException("cannot get BigInteger from current parser state");
        }
        try {
            // NOTE! Length of number string has been validated earlier
            _numberBigInt = NumberInput.parseBigInteger(
                    _numberString,
                    isEnabled(StreamReadFeature.USE_FAST_BIG_NUMBER_PARSER));
        } catch (NumberFormatException nex) {
            throw _constructReadException("Malformed numeric value ("+_longNumberDesc(_numberString)+")", nex);
        }
        _numberString = null;
        return _numberBigInt;
    }

    /**
     * Internal accessor that needs to be used for accessing number value of type
     * {@link BigDecimal} which -- as of 2.14 -- is typically lazily parsed.
     *
     * @return BigDecimal value decoded or converted for the current event
     */
    protected BigDecimal _getBigDecimal() {
        if (_numberBigDecimal != null) {
            return _numberBigDecimal;
        }
        if (_numberString == null) {
            throw new IllegalStateException("cannot get BigDecimal from current parser state");
        }
        try {
            // NOTE! Length of number string has been validated earlier
            _numberBigDecimal = NumberInput.parseBigDecimal(
                    _numberString,
                    isEnabled(StreamReadFeature.USE_FAST_BIG_NUMBER_PARSER));
        } catch (NumberFormatException nex) {
            throw _constructReadException("Malformed numeric value ("+_longNumberDesc(_numberString)+")", nex);
        }
        _numberString = null;
        return _numberBigDecimal;
    }

    /**
     * Internal accessor that needs to be used for accessing number value of type
     * {@code double} which will be lazily parsed.
     *
     * @return {@code double} value decoded or converted for the current event
     */
    protected double _getNumberDouble() {
        if (_numberString != null) {
            try {
                _numberDouble = NumberInput.parseDouble(_numberString,
                        isEnabled(StreamReadFeature.USE_FAST_DOUBLE_PARSER));
            } catch (NumberFormatException nex) {
                throw _constructReadException("Malformed numeric value ("+_longNumberDesc(_numberString)+")", nex);
            }
            _numberString = null;
        }
        return _numberDouble;
    }

    /**
     * Internal accessor that needs to be used for accessing number value of type
     * {@code float} which will be lazily parsed.
     *
     * @return {@code float} value decoded or converted for the current event
     *
     * @since 2.15
     */
    protected float _getNumberFloat() {
        if (_numberString != null) {
            try {
                _numberFloat = NumberInput.parseFloat(_numberString,
                        isEnabled(StreamReadFeature.USE_FAST_DOUBLE_PARSER));
            } catch (NumberFormatException nex) {
                throw _constructReadException("Malformed numeric value ("+_longNumberDesc(_numberString)+")", nex);
            }
            _numberString = null;
        }
        return _numberFloat;
    }

    /*
    /**********************************************************************
    /* Base64 handling support
    /**********************************************************************
     */

    /**
     * Method that sub-classes must implement to support escaped sequences
     * in base64-encoded sections.
     * Sub-classes that do not need base64 support can leave this as is
     *
     * @return Character decoded, if any
     *
     * @throws JacksonException If escape decoding fails
     */
    protected char _decodeEscaped() throws JacksonException {
        throw new UnsupportedOperationException();
    }

    protected final int _decodeBase64Escape(Base64Variant b64variant, int ch, int index)
        throws JacksonException
    {
        // Need to handle escaped chars
        if (ch != '\\') {
            _reportInvalidBase64Char(b64variant, ch, index);
        }
        int unescaped = _decodeEscaped();
        // if white space, skip if first triplet; otherwise errors
        if (unescaped <= INT_SPACE) {
            if (index == 0) { // whitespace only allowed to be skipped between triplets
                return -1;
            }
        }
        // otherwise try to find actual triplet value
        int bits = b64variant.decodeBase64Char(unescaped);
        if (bits < 0) {
            if (bits != Base64Variant.BASE64_VALUE_PADDING) {
                _reportInvalidBase64Char(b64variant, unescaped, index);
            }
        }
        return bits;
    }

    protected final int _decodeBase64Escape(Base64Variant b64variant, char ch, int index)
        throws JacksonException
    {
        if (ch != '\\') {
            _reportInvalidBase64Char(b64variant, ch, index);
            return -1; // never gets here
        }
        char unescaped = _decodeEscaped();
        // if white space, skip if first triplet; otherwise errors
        if (unescaped <= INT_SPACE) {
            if (index == 0) { // whitespace only allowed to be skipped between triplets
                return -1;
            }
        }
        // otherwise try to find actual triplet value
        int bits = b64variant.decodeBase64Char(unescaped);
        if (bits < 0) {
            // second check since padding can only be 3rd or 4th byte (index #2 or #3)
            if ((bits != Base64Variant.BASE64_VALUE_PADDING) || (index < 2)) {
                _reportInvalidBase64Char(b64variant, unescaped, index);
            }
        }
        return bits;
    }

    protected <T> T _reportInvalidBase64Char(Base64Variant b64variant, int ch, int bindex)
            throws StreamReadException {
        return _reportInvalidBase64Char(b64variant, ch, bindex, null);
    }

    /*
     * @param bindex Relative index within base64 character unit; between 0
     *  and 3 (as unit has exactly 4 characters)
     */
    protected <T> T _reportInvalidBase64Char(Base64Variant b64variant, int ch, int bindex, String msg)
            throws StreamReadException
    {
        String base;
        if (ch <= INT_SPACE) {
            base = String.format("Illegal white space character (code 0x%s) as character #%d of 4-char base64 unit: can only used between units",
                    Integer.toHexString(ch), (bindex+1));
        } else if (b64variant.usesPaddingChar(ch)) {
            base = "Unexpected padding character ('"+b64variant.getPaddingChar()+"') as character #"+(bindex+1)+" of 4-char base64 unit: padding only legal as 3rd or 4th character";
        } else if (!Character.isDefined(ch) || Character.isISOControl(ch)) {
            // Not sure if we can really get here... ? (most illegal xml chars are caught at lower level)
            base = "Illegal character (code 0x"+Integer.toHexString(ch)+") in base64 content";
        } else {
            base = "Illegal character '"+((char)ch)+"' (code 0x"+Integer.toHexString(ch)+") in base64 content";
        }
        if (msg != null) {
            base = base + ": " + msg;
        }
        return _reportError(base);
    }

    protected <T> T _handleBase64MissingPadding(Base64Variant b64variant)
            throws StreamReadException
    {
        return _reportError(b64variant.missingPaddingMessage());
    }

    /*
    /**********************************************************************
    /* Internal/package methods: other
    /**********************************************************************
     */

    /**
     * Helper method used to encapsulate logic of including (or not) of
     * "content reference" when constructing {@link TokenStreamLocation} instances.
     *
     * @return ContentReference object to use.
     */
    protected ContentReference _contentReference() {
        if (isEnabled(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION)) {
            return _ioContext.contentReference();
        }
        return _contentReferenceRedacted();
    }

    /**
     * Helper method used to encapsulate logic of providing
     * "content reference" when constructing {@link TokenStreamLocation} instances
     * and source information is <b>NOT</b> to be included
     * ({@code StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION} disabled).
     *<p>
     * Default implementation will simply return {@link ContentReference#redacted()}.
     *
     * @return ContentReference object to use when source is not to be included
     */
    protected ContentReference _contentReferenceRedacted() {
        return ContentReference.redacted();
    }

    /* Helper method called by name-decoding methods that require storage
     * for "quads" (4-byte units encode as ints), when existing buffer
     * is full.
     */
    protected static int[] growArrayBy(int[] arr, int more) throws IllegalArgumentException
    {
        if (arr == null) {
            return new int[more];
        }
        final int len = arr.length + more;
        if (len < 0) {
            throw new IllegalArgumentException("Unable to grow array to longer than `Integer.MAX_VALUE`");
        }
        return Arrays.copyOf(arr, len);
    }

    // Helper method to call to expand "quad" buffer for name decoding
    protected int[] _growNameDecodeBuffer(int[] arr, int more) throws StreamConstraintsException {
        // the following check will fail if the array is already bigger than is allowed for names
        _streamReadConstraints.validateNameLength(arr.length << 2);
        return growArrayBy(arr, more);
    }
}