CsvDecoder.java

package tools.jackson.dataformat.csv.impl;

import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;

import tools.jackson.core.*;
import tools.jackson.core.JsonParser.NumberType;
import tools.jackson.core.StreamReadFeature;
import tools.jackson.core.exc.StreamReadException;
import tools.jackson.core.io.NumberInput;
import tools.jackson.core.io.IOContext;
import tools.jackson.core.util.SimpleStreamReadContext;
import tools.jackson.core.util.TextBuffer;
import tools.jackson.dataformat.csv.CsvParser;
import tools.jackson.dataformat.csv.CsvReadFeature;
import tools.jackson.dataformat.csv.CsvSchema;

/**
 * Low-level helper class that handles actual reading of CSV,
 * purely based on indexes given without worrying about reordering etc.
 */
public class CsvDecoder
{
    private final static int INT_SPACE = 0x0020;

    private final static int INT_CR = '\r';
    private final static int INT_LF = '\n';

    /*
    /**********************************************************************
    /* Input handling, configuration
    /**********************************************************************
     */

    /**
     * Unfortunate back reference, needed for error reporting
     */
    protected final CsvParser _owner;

    /**
     * I/O context for this reader. It handles buffer allocation
     * for the reader.
     */
    protected final IOContext _ioContext;

    /**
     * Input stream that can be used for reading more content, if one
     * in use. May be null, if input comes just as a full buffer,
     * or if the stream has been closed.
     *<p>
     * NOTE: renamed in 2.13 from {@code _inputSource}.
     */
    protected Reader _inputReader;

    /**
     * Flag that indicates whether the input buffer is recycable (and
     * needs to be returned to recycler once we are done) or not.
     * <p>
     * If it is not, it also means that parser can NOT modify underlying
     * buffer.
     */
    protected boolean _bufferRecyclable;

    protected boolean _autoCloseInput;

    /**
     * Flag that indicates whether {@link #_inputReader} was constructed by Jackson
     * (wrapping a caller-provided {@link java.io.InputStream}) rather than handed to
     * us by the caller. Readers we construct must always be closed -- that is what
     * returns their buffers to the recycler -- whereas the {@link java.io.InputStream}
     * underneath is only closed if {@link #_autoCloseInput} is set.
     *
     * @since 3.3
     */
    protected final boolean _ownsInputReader;

    /**
     * Configuration flag that determines whether spaces surrounding
     * separator characters are to be automatically trimmed or not.
     */
    protected boolean _trimSpaces;

    protected boolean _allowComments;

    protected boolean _skipBlankLines;

    protected boolean _skipEmptyRows;

    /**
     * Number of separator characters consumed by {@link #_trySkipEmptyRow()} when
     * a row starting with separators turns out to contain non-empty values.
     * These consumed separators are replayed as empty String values by
     * {@link #nextString()}.
     *
     * @since 3.2
     */
    protected int _pendingEmptyColumns;

    /**
     * Maximum of quote character, linefeeds (\r and \n), escape character.
     */
    protected int _maxSpecialChar;

    protected int _separatorChar;

    protected int _quoteChar;

    protected int _escapeChar;

    /*
    /**********************************************************************
    /* Input handling, state
    /**********************************************************************
     */

    /**
     * Buffer that contains contents of all values after processing
     * of doubled-quotes, escaped characters.
     */
    protected final TextBuffer _textBuffer;

    /**
     * Current buffer from which data is read; generally data is read into
     * buffer from input source, but in some cases pre-loaded buffer
     * is handed to the parser.
     */
    protected char[] _inputBuffer;

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

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

    /**
     * Lazily-allocated buffer for tracking leading whitespace consumed
     * while looking ahead for a quote character ([dataformats-text#643]).
     * Reused across calls to avoid repeated allocation.
     *
     * @since 3.2
     */
    protected char[] _leadingSpaceBuf;

    /**
     * Marker to indicate that a linefeed was encountered and now
     * needs to be handled (indicates end-of-record).
     */
    protected int _pendingLF = 0;

    /**
     * Flag that indicates whether parser is closed or not. Gets
     * set when parser is either closed by explicit call
     * ({@link #close}) or when end-of-input is reached.
     */
    protected boolean _closed;
    
    /*
    /**********************************************************************
    /* 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 = 0L;

    /**
     * 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 = 0;

    /**
     * Flag that indicates whether the current token has been quoted or not.
     *
     * @since 2.18
     */
    protected boolean _currInputQuoted = false;

    // // // 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 = 0;

    /**
     * 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 = 0;

    /*
    /**********************************************************************
    /* Constants and fields of former 'JsonNumericParserBase'
    /**********************************************************************
     */

    final protected static int NR_UNKNOWN = 0;

    // First, integer types

    final protected static int NR_INT = 0x0001;
    final protected static int NR_LONG = 0x0002;
    final protected static int NR_BIGINT = 0x0004;

    // And then floating point types

    final protected static int NR_DOUBLE = 0x008;
    final protected static int NR_BIGDECIMAL = 0x0010;

    // Also, we need some numeric constants (copied from ParserBase)

    final static BigInteger BI_MIN_INT = BigInteger.valueOf(Integer.MIN_VALUE);
    final static BigInteger BI_MAX_INT = BigInteger.valueOf(Integer.MAX_VALUE);

    final static BigInteger BI_MIN_LONG = BigInteger.valueOf(Long.MIN_VALUE);
    final static BigInteger BI_MAX_LONG = BigInteger.valueOf(Long.MAX_VALUE);

    final static BigDecimal BD_MIN_LONG = new BigDecimal(Long.MIN_VALUE);
    final static BigDecimal BD_MAX_LONG = new BigDecimal(Long.MAX_VALUE);

    final static BigDecimal BD_MIN_INT = new BigDecimal(Long.MIN_VALUE);
    final static BigDecimal BD_MAX_INT = new BigDecimal(Long.MAX_VALUE);

    final static long MIN_INT_L = Integer.MIN_VALUE;
    final static long MAX_INT_L = Integer.MAX_VALUE;

    // These are not very accurate, but have to do... (for bounds checks)

    final static double MIN_LONG_D = Long.MIN_VALUE;
    final static double MAX_LONG_D = Long.MAX_VALUE;

    final static double MIN_INT_D = Integer.MIN_VALUE;
    final static double MAX_INT_D = Integer.MAX_VALUE;


    // Digits, numeric
    final protected static int INT_0 = '0';
    final protected static int INT_1 = '1';
    final protected static int INT_2 = '2';
    final protected static int INT_3 = '3';
    final protected static int INT_4 = '4';
    final protected static int INT_5 = '5';
    final protected static int INT_6 = '6';
    final protected static int INT_7 = '7';
    final protected static int INT_8 = '8';
    final protected static int INT_9 = '9';

    final protected static int INT_MINUS = '-';
    final protected static int INT_PLUS = '+';
    final protected static int INT_DECIMAL_POINT = '.';

    final protected static int INT_e = 'e';
    final protected static int INT_E = 'E';

    final protected static char CHAR_NULL = '\0';

    // Numeric value holders: multiple fields used for
    // 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 double _numberDouble;

    // And then object types

    protected BigInteger _numberBigInt;

    protected BigDecimal _numberBigDecimal;

    /**
     * Textual number representation captured from input in cases lazy-parsing
     * is desired.
     *<p>
     * As of 2.14, this only applies to {@link BigInteger} and {@link BigDecimal}.
     *
     * @since 2.14
     */
    protected String _numberString;

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

    /**
     * @deprecated Since 3.3 use
     *   {@link #CsvDecoder(IOContext, CsvParser, Reader, CsvSchema, TextBuffer, int, int, boolean)}
     *   instead
     */
    @Deprecated // since 3.3
    public CsvDecoder(IOContext ctxt, CsvParser owner, Reader r,
                      CsvSchema schema, TextBuffer textBuffer,
                      int stdFeatures, int csvFeatures)
    {
        this(ctxt, owner, r, schema, textBuffer, stdFeatures, csvFeatures, false);
    }

    /**
     * @param ownsReader Whether {@code r} was constructed by Jackson (and hence must
     *    always be closed), or provided by the caller
     *
     * @since 3.3
     */
    public CsvDecoder(IOContext ctxt, CsvParser owner, Reader r,
                      CsvSchema schema, TextBuffer textBuffer,
                      int stdFeatures, int csvFeatures, boolean ownsReader)
    {
        _owner = owner;
        _ioContext = ctxt;
        _inputReader = r;
        _ownsInputReader = ownsReader;
        _textBuffer = textBuffer;
        _autoCloseInput = StreamReadFeature.AUTO_CLOSE_SOURCE.enabledIn(stdFeatures);
        _allowComments = CsvReadFeature.ALLOW_COMMENTS.enabledIn(csvFeatures);
        _trimSpaces = CsvReadFeature.TRIM_SPACES.enabledIn(csvFeatures);
        _skipBlankLines = CsvReadFeature.SKIP_EMPTY_LINES.enabledIn(csvFeatures);
        _skipEmptyRows = CsvReadFeature.SKIP_EMPTY_ROWS.enabledIn(csvFeatures);
        _inputBuffer = ctxt.allocTokenBuffer();
        _bufferRecyclable = true; // since we allocated it
        _tokenInputRow = -1;
        _tokenInputCol = -1;
        setSchema(schema);
    }

    public void setSchema(CsvSchema schema) {
        _separatorChar = schema.getColumnSeparator();
        _quoteChar = schema.getQuoteChar();
        _escapeChar = schema.getEscapeChar();
        if (!_allowComments) {
            _allowComments = schema.allowsComments();
        }
        int max = Math.max(_separatorChar, _quoteChar);
        max = Math.max(max, _escapeChar);
        max = Math.max(max, '\r');
        max = Math.max(max, '\n');
        _maxSpecialChar = max;
    }

    /*
    /**********************************************************************
    /* JsonParser implementations passed-through by CsvParser
    /**********************************************************************
     */

    public Object getInputSource() {
        return _inputReader;
    }

    public void close() throws IOException {
        _pendingLF = 1; // just to ensure we'll also check _closed flag later on
        if (!_closed) {
            _closed = true;
            try {
                _closeInput();
            } finally {
                // Also, internal buffer(s) can now be released as well
                releaseBuffers();
            }
        }
    }

    public int releaseBuffered(Writer out) throws IOException {
        int count = _inputEnd - _inputPtr;
        if (count < 1) {
            return 0;
        }
        // let's just advance ptr to end
        int origPtr = _inputPtr;
        out.write(_inputBuffer, origPtr, count);
        return count;
    }

    public SimpleStreamReadContext childArrayContext(SimpleStreamReadContext context) {
        int col = _inputPtr - _currInputRowStart + 1; // 1-based
        return context.createChildArrayContext(_currInputRow, col);
    }

    public SimpleStreamReadContext childObjectContext(SimpleStreamReadContext context) {
        int col = _inputPtr - _currInputRowStart + 1; // 1-based
        return context.createChildObjectContext(_currInputRow, col);
    }

    public TokenStreamLocation getTokenLocation()
    {
        return new TokenStreamLocation(_ioContext.contentReference(),
                getTokenCharacterOffset(),
                getTokenLineNr(), getTokenColumnNr());
    }

    public TokenStreamLocation getCurrentLocation() {
        int ptr = _inputPtr;
        // One twist: when dealing with a "pending LF", need to
        // go back one position when calculating location
        if (_pendingLF > 1) { // 1 is used as marker for end-of-input
            --ptr;
        }
        int col = ptr - _currInputRowStart + 1; // 1-based
        return new TokenStreamLocation(_ioContext.contentReference(),
                _currInputProcessed + ptr - 1L, _currInputRow, col);
    }

    public final int getCurrentRow() {
        return _currInputRow;
    }

    public final int getCurrentColumn() {
        int ptr = _inputPtr;
        // One twist: when dealing with a "pending LF", need to
        // go back one position when calculating location
        if (_pendingLF > 1) { // 1 is used as marker for end-of-input
            --ptr;
        }
        return ptr - _currInputRowStart + 1; // 1-based
    }

    /**
     * Tell if the current token has been quoted or not.
     * @return True if the current token has been quoted, false otherwise
     *
     * @since 2.18
     */
    public final boolean isCurrentTokenQuoted() {
        return _currInputQuoted;
    }
    
    /*
    /**********************************************************************
    /* Helper methods, input handling
    /**********************************************************************
     */

    protected final long getTokenCharacterOffset() {
        return _tokenInputTotal;
    }

    protected final int getTokenLineNr() {
        return _tokenInputRow;
    }

    protected final 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);
    }

    protected void releaseBuffers() throws JacksonException {
        _textBuffer.releaseBuffers();
        char[] buf = _inputBuffer;
        if (buf != null) {
            _inputBuffer = null;
            _ioContext.releaseTokenBuffer(buf);
        }
    }

    protected void _closeInput() throws IOException {
        _pendingLF = 1; // just to ensure we'll also check _closed flag later on

        /* 25-Nov-2008, tatus: As per [JACKSON-16] we are not to call close()
         *   on the underlying Reader, unless we "own" it, or auto-closing
         *   feature is enabled.
         */
        // 08-Sep-2026, pjfanning: [dataformats-text#718] but a Reader we constructed
        //   ourselves must always be closed, else the buffer it took from the recycler
        //   is lost; such a Reader knows not to close the caller's `InputStream` unless
        //   auto-closing is enabled
        if (_inputReader != null) {
            if (_ownsInputReader || _autoCloseInput || _ioContext.isResourceManaged()) {
                _inputReader.close();
            }
            _inputReader = null;
        }
    }

    protected final boolean loadMore() throws JacksonException {
        _currInputProcessed += _inputEnd;
        _currInputRowStart -= _inputEnd;

        // [dataformats-text#613]: enforce max document length
        _ioContext.streamReadConstraints().validateDocumentLength(_currInputProcessed);

        try {
            if (_inputReader != null) {
                int count = _inputReader.read(_inputBuffer, 0, _inputBuffer.length);
                _inputEnd = count;
                _inputPtr = 0;
                if (count > 0) {
                    return true;
                }
                // End of input; close here --  but note, do NOT yet call releaseBuffers()
                // as there may be buffered input to handle
                _closeInput();
                // Should never return 0, so let's fail
                if (count == 0) {
                    throw new IOException("InputStream.read() returned 0 characters when trying to read "+_inputBuffer.length+" bytes");
                }
            }
        } catch (IOException e) {
            throw _owner._wrapIOFailure(e);
        }
        return false;
    }

    /*
    /**********************************************************************
    /* Actual parsing, access methods
    /**********************************************************************
     */

    public String getText() throws JacksonException {
        return _textBuffer.contentsAsString();
    }

    /**
     * Method that can be called to see if there is at least one more
     * character to be parsed.
     */
    public boolean hasMoreInput() throws JacksonException {
        if (_inputPtr < _inputEnd) {
            return true;
        }
        return loadMore();
    }

    /**
     * Method called to handle details of starting a new line, which may
     * include skipping a linefeed.
     *
     * @return True if there is a new data line to handle; false if not
     */
    public boolean startNewLine() throws JacksonException {
        _pendingEmptyColumns = 0;
        // first: if pending LF, skip it
        if (_pendingLF != 0) {
            if (_inputReader == null) {
                return false;
            }
            _handleLF();
        }
        return skipLinesWhenNeeded();
    }

    /**
     * Optionally skip lines that are empty or are comments, depending on the feature activated in the parser
     *
     * @return false if the end of input was reached
     * @throws JacksonException
     */
    public boolean skipLinesWhenNeeded() throws JacksonException {
        if (_allowComments) {
            return _skipCommentLines();
        }
        if (!_skipBlankLines && !_skipEmptyRows) {
            return hasMoreInput();
        }

        // only need to skip fully empty lines (and optionally empty rows)
        while (hasMoreInput()) {
            char ch = _inputBuffer[_inputPtr];
            if (ch == '\r' || ch == '\n') {
                ++_inputPtr;
                _pendingLF = ch;
                _handleLF();
                continue;
            }
            if (ch == ' ' && _skipBlankLines) {
                ++_inputPtr;
                continue;
            }
            // [dataformats-text#368]: Row of only separator characters?
            if (_skipEmptyRows && ch == _separatorChar) {
                if (_trySkipEmptyRow()) {
                    continue;
                }
            }
            return true; // processing can go on
        }
        return false; // end of input
    }

    public boolean _skipCommentLines() throws JacksonException
    {
        while ((_inputPtr < _inputEnd) || loadMore()) {
            char ch = _inputBuffer[_inputPtr];
            switch (ch) {
            case '#':
                ++_inputPtr;
                _skipCommentContents();
                continue;
            case '\r':
            case '\n':
                ++_inputPtr;
                _pendingLF = ch;
                _handleLF();
                continue;
            case ' ':
                // skip all blanks (in both comments/blanks skip mode)
                ++_inputPtr;
                continue;
            default:
                // [dataformats-text#368]: Check if line consists only of separators
                if (_skipEmptyRows && ch == _separatorChar) {
                    if (_trySkipEmptyRow()) {
                        continue;
                    }
                }
                return true;
            }
        }
        return false; // end of input
    }

    protected void _skipCommentContents() throws JacksonException
    {
        while ((_inputPtr < _inputEnd) || loadMore()) {
            char ch = _inputBuffer[_inputPtr++];
            if (ch == '\r' || ch == '\n') {
                _pendingLF = ch;
                _handleLF();
                break;
            }
        }
    }

    /**
     * Helper method called when we see a separator character at the start of a line
     * and need to determine if the entire row consists only of consecutive separator
     * characters (no other content) followed by a linefeed or EOF.
     *<p>
     * Consumes separator characters one at a time. If a linefeed or EOF is reached,
     * the row is empty and is skipped (returns {@code true}). If any other character
     * is found, the consumed separators are recorded in {@link #_pendingEmptyColumns}
     * so that {@link #nextString()} can replay them as empty values.
     *
     * @return {@code true} if the row was determined to be empty and was skipped;
     *   {@code false} if the row contains non-empty content (consumed separators
     *   will be replayed via {@link #_pendingEmptyColumns})
     */
    protected boolean _trySkipEmptyRow() throws JacksonException
    {
        int separatorCount = 0;
        while (true) {
            // Consume the separator character at current position
            ++_inputPtr;
            ++separatorCount;

            // Need more input?
            if (_inputPtr >= _inputEnd) {
                if (!loadMore()) {
                    // EOF after separators only: empty row at end of input
                    return true;
                }
            }
            char ch = _inputBuffer[_inputPtr];
            if (ch == _separatorChar) {
                continue;
            }
            if (ch == '\r' || ch == '\n') {
                // Row consisted only of separators: skip it
                ++_inputPtr;
                _pendingLF = ch;
                _handleLF();
                return true;
            }
            // Found non-separator content: not an empty row.
            // Record consumed separators so nextString() replays them.
            _pendingEmptyColumns = separatorCount;
            return false;
        }
    }

    /**
     * Method called to blindly skip a single line of content, without considering
     * aspects like quoting or escaping. Used currently simply to skip the first
     * line of input document, if instructed to do so.
     */
    public boolean skipLine() throws JacksonException {
        if (_pendingLF != 0) {
            if (_inputReader == null) {
                return false;
            }
            _handleLF();
        }
        while (_inputPtr < _inputEnd || loadMore()) {
            char c = _inputBuffer[_inputPtr++];
            if (c == '\r' || c == '\n') {
                // important: handle trailing linefeed now, so caller need not bother
                _pendingLF = c;
                _handleLF();
                return true;
            }
        }
        return false;
    }

    /**
     * Method called to parse the next token when we don't have any type
     * information, so that all tokens are exposed as basic String
     * values.
     *
     * @return Column value if more found; null to indicate end of line
     * of input
     */
    public String nextString() throws JacksonException {
        _numTypesValid = NR_UNKNOWN;
        _currInputQuoted = false;  // Reset; set to true below only if opening quote found

        // [dataformats-text#368]: Replay separator characters consumed by _trySkipEmptyRow()
        // as empty column values
        if (_pendingEmptyColumns > 0) {
            --_pendingEmptyColumns;
            _textBuffer.resetWithString("");
            return "";
        }

        if (_pendingLF > 0) { // either pendingLF, or closed
            if (_inputReader != null) { // if closed, we just need to return null
                _handleLF();
            }
            return null; // end of line without new value
        }
        int i;

        if (_trimSpaces) {
            i = _skipLeadingSpace();
        } else {
            i = _nextChar();
            // [dataformats-text#643]: Even without TRIM_SPACES, spaces before
            // a quoted value are insignificant and should be skipped.
            // Consume whitespace across buffer boundaries; if a quote follows,
            // discard the spaces and parse as quoted. If not, restore consumed
            // spaces into the unquoted output.
            if (i <= ' ' && i > 0
                    && i != _separatorChar && i != INT_CR && i != INT_LF) {
                // Lazily allocate / reuse buffer for consumed whitespace
                char[] spaceBuf = _leadingSpaceBuf;
                if (spaceBuf == null) {
                    _leadingSpaceBuf = spaceBuf = new char[16];
                }
                int spaceCount = 0;
                spaceBuf[spaceCount++] = (char) i;
                boolean foundQuote = false;

                while (true) {
                    if (_inputPtr >= _inputEnd) {
                        if (!loadMore()) {
                            break;
                        }
                    }
                    final char ch = _inputBuffer[_inputPtr];
                    if (ch == _quoteChar) {
                        // Found quote: discard leading spaces, proceed as quoted
                        _inputPtr++;
                        i = _quoteChar;
                        foundQuote = true;
                        break;
                    }
                    if (ch > ' ' || ch == _separatorChar
                            || ch == '\r' || ch == '\n') {
                        break;
                    }
                    // Consume this whitespace character, tracking it for restoration
                    _inputPtr++;
                    if (spaceCount >= spaceBuf.length) {
                        _leadingSpaceBuf = spaceBuf =
                                Arrays.copyOf(spaceBuf, spaceBuf.length * 2);
                    }
                    spaceBuf[spaceCount++] = ch;
                }

                if (!foundQuote && spaceCount > 0) {
                    // Not a quoted value: restore consumed whitespace into unquoted output
                    _tokenInputTotal = _currInputProcessed + _inputPtr - 1;
                    _tokenInputRow = _currInputRow;
                    _tokenInputCol = _inputPtr - _currInputRowStart - 1;
                    _textBuffer.resetWithCopy(spaceBuf, 0, spaceCount);
                    return _nextUnquotedString(_textBuffer.getBufferWithoutReset(),
                            _textBuffer.getCurrentSegmentSize());
                }
            }
        }
        // First, need to ensure we know the starting location of token
        _tokenInputTotal = _currInputProcessed + _inputPtr - 1;
        _tokenInputRow = _currInputRow;
        _tokenInputCol = _inputPtr - _currInputRowStart - 1;

        if (i < 0) { // EOF at this point signifies empty value
            _textBuffer.resetWithString("");
            return "";
        }

        if (i == INT_CR || i == INT_LF) { // end-of-line means end of record; but also need to handle LF later on
            _pendingLF = i;
            _textBuffer.resetWithString("");
            return "";
        }
        // two modes: quoted, unquoted
        _currInputQuoted = i == _quoteChar; // Keep track of quoting
        if (_currInputQuoted) { // offline quoted case (longer)
            return _nextQuotedString();
        }
        if (i == _separatorChar) {
            _textBuffer.resetWithString("");
            return "";
        }
        char[] outBuf = _textBuffer.emptyAndGetCurrentSegment();
        outBuf[0] = (char) i;
        int outPtr = 1;

        if (i == _escapeChar) {
            // Reset the escaped character
            outBuf[0] = _unescape();
            return _nextUnquotedString(outBuf, outPtr);
        }

        int ptr = _inputPtr;
        if (ptr >= _inputEnd) {
            if (!loadMore()) { // ok to have end-of-input but...
                return _textBuffer.finishAndReturn(outPtr, _trimSpaces);
            }
            ptr = _inputPtr;
        }
        final int end;

        {
            int max = Math.min(_inputEnd - ptr, outBuf.length - outPtr);
            end = ptr + max;
        }

        // handle unquoted case locally if it can be handled without
        // crossing buffer boundary...
        char[] inputBuffer = _inputBuffer;

        while (ptr < end) {
            char c = inputBuffer[ptr++];
            if (c <= _maxSpecialChar) {
                if (c == _separatorChar) { // end of value, yay!
                    _inputPtr = ptr;
                    return _textBuffer.finishAndReturn(outPtr, _trimSpaces);
                }
                if (c == '\r' || c == '\n') {
                    _pendingLF = c;
                    _inputPtr = ptr;
                    return _textBuffer.finishAndReturn(outPtr, _trimSpaces);
                }
                if (c == _escapeChar) {
                    --ptr;
                    break;
                }
            }
            outBuf[outPtr++] = c;
        }
        // ok, either input or output across buffer boundary, offline
        _inputPtr = ptr;
        return _nextUnquotedString(outBuf, outPtr);
    }

    public JsonToken nextStringOrLiteral() throws JacksonException {
        _numTypesValid = NR_UNKNOWN;
        // !!! TODO: implement properly
        String value = nextString();
        if (value == null) {
            return null;
        }
        return JsonToken.VALUE_STRING;
    }

    public JsonToken nextNumber() throws JacksonException {
        _numTypesValid = NR_UNKNOWN;
        // !!! TODO: implement properly
        String value = nextString();
        if (value == null) {
            return null;
        }
        return JsonToken.VALUE_STRING;
    }

    public JsonToken nextNumberOrString() throws JacksonException {
        _numTypesValid = NR_UNKNOWN;
        // !!! TODO: implement properly
        String value = nextString();
        if (value == null) {
            return null;
        }
        return JsonToken.VALUE_STRING;
    }
    
    /*
    /**********************************************************************
    /* Actual parsing, private helper methods
    /**********************************************************************
     */

    protected String _nextUnquotedString(char[] outBuf, int outPtr) throws JacksonException {
        int c;
        final char[] inputBuffer = _inputBuffer;

        main_loop:
        while (true) {
            int ptr = _inputPtr;
            if (ptr >= _inputEnd) {
                if (!loadMore()) { // ok to have end-of-input, are done
                    _inputPtr = ptr;
                    break main_loop;
                }
                ptr = _inputPtr;
            }
            if (outPtr >= outBuf.length) {
                outBuf = _textBuffer.finishCurrentSegment();
                outPtr = 0;
            }
            final int max = Math.min(_inputEnd, (ptr + (outBuf.length - outPtr)));
            while (ptr < max) {
                c = inputBuffer[ptr++];
                if (c <= _maxSpecialChar) {
                    if (c == _separatorChar) { // end of value, yay!
                        _inputPtr = ptr;
                        break main_loop;
                    }
                    if (c == '\r' || c == '\n') { // end of line is end of value as well
                        _inputPtr = ptr;
                        _pendingLF = c;
                        break main_loop;
                    }
                    if (c == _escapeChar) {
                        _inputPtr = ptr;
                        outBuf[outPtr++] = _unescape();
                        // May have passed input boundary, need to re-set
                        continue main_loop;
                    }
                }
                outBuf[outPtr++] = (char) c;
            }
            _inputPtr = ptr;
        }
        return _textBuffer.finishAndReturn(outPtr, _trimSpaces);
    }

    protected String _nextQuotedString() throws JacksonException {
        char[] outBuf = _textBuffer.emptyAndGetCurrentSegment();
        int outPtr = 0;

        final char[] inputBuffer = _inputBuffer;
        boolean checkLF = false; // marker for split CR+LF

        main_loop:
        while (true) {
            int ptr = _inputPtr;
            if (ptr >= _inputEnd) {
                if (!loadMore()) { // not ok, missing end quote
                    _owner._reportError("Missing closing quote for value"); // should indicate start position?
                }
                ptr = _inputPtr;
                if (checkLF) {
                    checkLF = false; // better reset
                    if (inputBuffer[ptr] == '\n') {
                        // undo earlier advancement, to keep line number correct
                        --_currInputRow;
                    }
                }
            }
            // 11-Feb-2024, tatu: Not quite sure what was supposed to happen here;
            //   but nothing was done. Leaving for now, remove from 2.18 or later
            /*
            if (checkLF) { // had a "hanging" CR in parse loop; check now
            }
            */
            if (outPtr >= outBuf.length) {
                outBuf = _textBuffer.finishCurrentSegment();
                outPtr = 0;
            }
            final int max = Math.min(_inputEnd, (ptr + (outBuf.length - outPtr)));

            inner_loop:
            while (true) {
                char c = inputBuffer[ptr++];
                if (c <= _maxSpecialChar) {
                    if (c == _quoteChar) {
                        _inputPtr = ptr;
                        break;
                    }
                    // Embedded linefeeds are fine
                    if (c == '\r') {
                        // bit crappy check but has to do:
                        if (ptr >= max) {
                            checkLF = true; // will need to be checked in beginning of next loop
                            ++_currInputRow;
                            _currInputRowStart = ptr;
                        } else if (inputBuffer[ptr] != '\n') {
                            ++_currInputRow;
                            _currInputRowStart = ptr;
                        }
                    } else if (c == '\n') {
                        ++_currInputRow;
                        _currInputRowStart = ptr;
                    } else if (c == _escapeChar) {
                        _inputPtr = ptr;
                        c = _unescape();
                        outBuf[outPtr++] = c;
                        // May have passed input boundary, need to re-set
                        continue main_loop;
                    }
                }
                outBuf[outPtr++] = c;
                if (ptr >= max) {
                    _inputPtr = ptr;
                    continue main_loop;
                }
                continue inner_loop;
            }
            // We get here if we hit a quote: check if it's doubled up, or end of value:
            if (_inputPtr < _inputEnd || loadMore()) {
                if (_inputBuffer[_inputPtr] == _quoteChar) { // doubled up, append
                    // note: should have enough room, is safe
                    outBuf[outPtr++] = (char) _quoteChar;
                    ++_inputPtr;
                    continue main_loop;
                }
            }
            // Not doubled; leave next char as is
            break;
        }
        // note: do NOT trim from within quoted Strings
        String result = _textBuffer.finishAndReturn(outPtr, false);
        // good, but we also need to locate and skip trailing space, separator
        // (note: space outside quotes never included, but must be skipped)
        while (_inputPtr < _inputEnd || loadMore()) { // end-of-input is fine
            int ch = _inputBuffer[_inputPtr++];
            if (ch == _separatorChar) { // common case, separator between columns
                break;
            }
            if (ch <= INT_SPACE) { // extra space, fine as well
                if (ch == INT_CR || ch == INT_LF) { // but end-of-line can't be yet skipped
                    _pendingLF = ch;
                    break;
                }
                continue;
            }
            _owner._reportUnexpectedCsvChar(ch, String.format(
                    "Expected column separator character (%s) or end-of-line", _getCharDesc(_separatorChar)));
        }
        return result;
    }

    protected final void _handleLF() throws JacksonException
    {
        // already skipped past first part; but may get \r\n so skip the other char too?
        if (_pendingLF == INT_CR) {
            if (_inputPtr < _inputEnd || loadMore()) {
                if (_inputBuffer[_inputPtr] == '\n') {
                    ++_inputPtr;
                }
            }
        }
        _pendingLF = 0;
        ++_currInputRow;
        _currInputRowStart = _inputPtr;
    }

    protected char _unescape() throws JacksonException {
        if (_inputPtr >= _inputEnd) {
            if (!loadMore()) {
                _reportError("Unexpected EOF in escaped character");
            }
        }
        // Some characters are more special than others, so:
        char c = _inputBuffer[_inputPtr++];
        return switch (c) {
            case '0' -> '\0';
            case 'n' -> '\n';
            case 'r' -> '\r';
            case 't' -> '\t';
            default -> c;
        };
    }

    protected final int _nextChar() throws JacksonException {
        if (_inputPtr >= _inputEnd) {
            if (!loadMore()) {
                return -1;
            }
        }
        return _inputBuffer[_inputPtr++];
    }

    protected final int _skipLeadingSpace() throws JacksonException {
        final int sep = _separatorChar;
        while (true) {
            if (_inputPtr >= _inputEnd) {
                if (!loadMore()) {
                    return -1;
                }
            }
            char ch = _inputBuffer[_inputPtr++];
            if ((ch > ' ') || (ch == sep)) {
                return ch;
            }
            switch (ch) {
                case '\r':
                case '\n':
                    return ch;
            }
        }
    }

    /*
    /**********************************************************************
    /* Numeric accessors for CsvParser
    /**********************************************************************
     */

    /**
     * Method used by {@link CsvParser#isExpectedNumberIntToken()} to coerce
     * current token into integer number, if it looks like one.
     */
    public boolean isExpectedNumberIntToken() throws JacksonException
    {
        if (looksLikeInt()) {
            _parseIntValue();
            return true;
        }
        return false;
    }

    /**
     * @param exact Whether we should try to retain maximum precision or not;
     *    passed as {@code true} by {@code getNumberValueExact()}, and as
     *    {@code false} by regular {@code getNumberValue)}.
     */
    public Number getNumberValue(boolean exact) throws JacksonException
    {
        if (_numTypesValid == NR_UNKNOWN) {
            _parseNumericValue(exact); // will also check event type
        }
        // Separate types for int types
        if ((_numTypesValid & NR_INT) != 0) {
            return Integer.valueOf(_numberInt);
        }
        if ((_numTypesValid & NR_LONG) != 0) {
            return Long.valueOf(_numberLong);
        }
        if ((_numTypesValid & NR_BIGINT) != 0) {
            return _getBigInteger();
        }
        // 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_DOUBLE) == 0) { // sanity check
            _throwInternal();
        }
        return Double.valueOf(_numberDouble);
    }

    public NumberType getNumberType() throws JacksonException {
        if (_numTypesValid == NR_UNKNOWN) {
            _parseNumericValue(false); // will also check event type
        }
        if ((_numTypesValid & NR_INT) != 0) {
            return NumberType.INT;
        }
        if ((_numTypesValid & NR_LONG) != 0) {
            return NumberType.LONG;
        }
        if ((_numTypesValid & NR_BIGINT) != 0) {
            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;
        }
        return NumberType.DOUBLE;
    }

    public int getIntValue() throws JacksonException {
        if ((_numTypesValid & NR_INT) == 0) {
            if (_numTypesValid == NR_UNKNOWN) { // not parsed at all
                _parseNumericValue(false); // will also check event type
            }
            if ((_numTypesValid & NR_INT) == 0) { // wasn't an int natively?
                convertNumberToInt(); // let's make it so, if possible
            }
        }
        return _numberInt;
    }

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

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

    public float getFloatValue() throws JacksonException {
        double value = getDoubleValue();
        // Bounds/range checks would be tricky here, so let's not bother...
        return (float) value;
    }

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

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

    /**
     * Internal accessor that needs to be used for accessing number value of type
     * {@link BigInteger} which -- as of 2.14 -- is typically lazily parsed.
     */
    protected BigInteger _getBigInteger() throws JacksonException {
        if (_numberBigInt != null) {
            return _numberBigInt;
        } else if (_numberString == null) {
            throw new IllegalStateException("cannot get BigInteger from current parser state");
        }
        _ioContext.streamReadConstraints().validateIntegerLength(_numberString.length());
        _numberBigInt = NumberInput.parseBigInteger(
                _numberString, _owner.isEnabled(StreamReadFeature.USE_FAST_BIG_NUMBER_PARSER));
        _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.
     */
    protected BigDecimal _getBigDecimal() throws JacksonException {
        if (_numberBigDecimal != null) {
            return _numberBigDecimal;
        } else if (_numberString == null) {
            throw new IllegalStateException("cannot get BigDecimal from current parser state");
        }
        _ioContext.streamReadConstraints().validateFPLength(_numberString.length());
        _numberBigDecimal = NumberInput.parseBigDecimal(
                _numberString, _owner.isEnabled(StreamReadFeature.USE_FAST_BIG_NUMBER_PARSER));
        _numberString = null;
        return _numberBigDecimal;
    }

    /*
    /**********************************************************************
    /* Conversion from textual to numeric representation
    /**********************************************************************
     */

    /**
     * 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 exactNumber Whether to try to retain the highest precision for
     *    floating-point values or not
     */
    protected void _parseNumericValue(boolean exactNumber)
        throws JacksonException
    {
        if (looksLikeInt()) {
            _parseIntValue();
            return;
        }
        /*
        if (_hasFloatToken() == JsonToken.VALUE_NUMBER_FLOAT) {
            _parseSlowFloatValue(expType);
            return;
        }
        _reportError("Current token ("+_currToken+") not numeric, can not use numeric value accessors");
        */
        _parseSlowFloatValue(exactNumber);
    }

    private boolean looksLikeInt() throws JacksonException {
        // NOTE: tokenizer has already materialized value as String (see
        // `_nextUnquotedString()` etc), so access that: no copying needed
        final String text = _textBuffer.contentsAsString();
        final int len = text.length();

        if (len == 0) {
            return false;
        }

        int i = 0;
        char c = text.charAt(0);
        if (c == '-' || c == '+') {
            if (len == 1) {
                return false;
            }
            ++i;
        }
        for (; i < len; ++i) {
            c = text.charAt(i);
            if (c > '9' || c < '0') {
                return false;
            }
        }
        return true;
    }

    // @since 2.12
    protected void _parseIntValue() throws JacksonException
    {
        // NOTE: parse directly from String tokenizer has already created,
        // instead of making a `char[]` copy of it (see `looksLikeInt()`)
        final String text = _textBuffer.contentsAsString();
        final char c = text.charAt(0);
        final boolean neg = (c == '-');
        final int offset = (neg || (c == '+')) ? 1 : 0;
        final int len = text.length() - offset;

        if (len <= 9) { // definitely fits in int
            // `NumberInput.parseInt(String)` handles leading minus sign but not
            // plus sign; that is rare enough to just use JDK parsing
            _numberInt = (c == '+') ? Integer.parseInt(text) : NumberInput.parseInt(text);
            _numTypesValid = NR_INT;
            return;
        }
        if (len <= 18) { // definitely fits in long
            long l = Long.parseLong(text);
            // [JACKSON-230] Could still fit in int, need to check
            if (len == 10) {
                if (neg) {
                    if (l >= MIN_INT_L) {
                        _numberInt = (int) l;
                        _numTypesValid = NR_INT;
                        return;
                    }
                } else {
                    if (l <= MAX_INT_L) {
                        _numberInt = (int) l;
                        _numTypesValid = NR_INT;
                        return;
                    }
                }
            }
            _numberLong = l;
            _numTypesValid = NR_LONG;
            return;
        }
        _parseSlowIntValue(text, offset, neg);
    }

    private final void _parseSlowFloatValue(boolean exactNumber)
        throws JacksonException
    {
        /* Nope: floating point. Here we need to be careful to get
         * optimal parsing strategy: choice is between accurate but
         * slow (BigDecimal) and lossy but fast (Double). For now
         * let's only use BD when explicitly requested -- it can
         * still be constructed correctly at any point since we do
         * retain textual representation
         */
        try {
            if (exactNumber) {
                _numberBigDecimal = null;
                _numberString = _textBuffer.contentsAsString();
                _numTypesValid = NR_BIGDECIMAL;
            } else {
                // Otherwise double has to do
                _numberDouble = _textBuffer.contentsAsDouble(_owner.isEnabled(StreamReadFeature.USE_FAST_DOUBLE_PARSER));
                _numTypesValid = NR_DOUBLE;
            }
        } catch (NumberFormatException nex) {
            // Can this ever occur? Due to overflow, maybe?
            throw _constructReadException(nex, "Malformed numeric value '" + _textBuffer.contentsAsString() + "'");
        }
    }

    // Called for 19 or more digits: may still fit in `long`
    private final void _parseSlowIntValue(String numStr, int offset, boolean neg)
        throws JacksonException
    {
        try {
            // `inLongRange()` needs digits without sign
            final String digits = (offset == 0) ? numStr : numStr.substring(offset);
            if (NumberInput.inLongRange(digits, neg)) {
                _numberLong = Long.parseLong(numStr);
                _numTypesValid = NR_LONG;
            } else {
                // nope, need the heavy guns... (rare case)
                _numberBigInt = null;
                _numberString = numStr;
                _numTypesValid = NR_BIGINT;
            }
        } catch (NumberFormatException nex) {
            // Can this ever occur? Due to overflow, maybe?
            throw _constructReadException(nex, "Malformed numeric value '" + numStr + "'");
        }
    }

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

    protected void convertNumberToInt() throws JacksonException {
        // First, converting from long ought to be easy
        if ((_numTypesValid & NR_LONG) != 0) {
            // Let's verify it's lossless conversion by simple roundtrip
            int result = (int) _numberLong;
            if (result != _numberLong) {
                _reportError("Numeric value (" + getText() + ") out of range of int");
            }
            _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
            if (_numberDouble < MIN_INT_D || _numberDouble > MAX_INT_D) {
                reportOverflowInt();
            }
            _numberInt = (int) _numberDouble;
        } 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(); // should never get here
        }

        _numTypesValid |= NR_INT;
    }

    protected void convertNumberToLong() throws JacksonException {
        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
            if (_numberDouble < MIN_LONG_D || _numberDouble > MAX_LONG_D) {
                reportOverflowLong();
            }
            _numberLong = (long) _numberDouble;
        } 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(); // should never get here
        }

        _numTypesValid |= NR_LONG;
    }

    protected void convertNumberToBigInteger()
            throws JacksonException {
        if ((_numTypesValid & NR_BIGDECIMAL) != 0) {
            // here it'll just get truncated, no exceptions thrown
            final BigDecimal bd = _getBigDecimal();
            _ioContext.streamReadConstraints().validateBigIntegerScale(bd.scale());
            _numberBigInt = bd.toBigInteger();
        } 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) {
            _numberBigInt = BigDecimal.valueOf(_numberDouble).toBigInteger();
        } else {
            _throwInternal(); // should never get here
        }
        _numTypesValid |= NR_BIGINT;
    }

    protected void convertNumberToDouble()
            throws JacksonException {
        // 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) {
            _numberDouble = _getBigDecimal().doubleValue();
        } else if ((_numTypesValid & NR_BIGINT) != 0) {
            _numberDouble = _getBigInteger().doubleValue();
        } else if ((_numTypesValid & NR_LONG) != 0) {
            _numberDouble = _numberLong;
        } else if ((_numTypesValid & NR_INT) != 0) {
            _numberDouble = _numberInt;
        } else {
            _throwInternal(); // should never get here
        }

        _numTypesValid |= NR_DOUBLE;
    }

    protected void convertNumberToBigDecimal() throws JacksonException {
        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 text = getText();
            _ioContext.streamReadConstraints().validateFPLength(text.length());
            _numberBigDecimal = NumberInput.parseBigDecimal(
                    text, _owner.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(); // should never get here
        }
        _numTypesValid |= NR_BIGDECIMAL;
    }

    /*
    /**********************************************************
    /* Number handling exceptions
    /**********************************************************
     */

    protected void reportUnexpectedNumberChar(int ch, String comment)
        throws StreamReadException
    {
        String msg = "Unexpected character (" + _getCharDesc(ch) + ") in numeric value";
        if (comment != null) {
            msg += ": " + comment;
        }
        _reportError(msg);
    }

    protected void reportInvalidNumber(String msg) throws StreamReadException {
        _reportError("Invalid numeric value: " + msg);
    }

    protected void reportOverflowInt() throws StreamReadException {
        _reportError("Numeric value (" + getText() + ") out of range of int (" + Integer.MIN_VALUE + " - " + Integer.MAX_VALUE + ")");
    }

    protected void reportOverflowLong() throws StreamReadException {
        _reportError("Numeric value (" + getText() + ") out of range of long (" + Long.MIN_VALUE + " - " + Long.MAX_VALUE + ")");
    }

    protected final StreamReadException _constructReadException(Throwable t, String msg) {
        return new StreamReadException(_owner, msg, t);
    }

    protected final static String _getCharDesc(int ch) {
        char c = (char) ch;
        if (Character.isISOControl(c)) {
            return "(CTRL-CHAR, code " + ch + ")";
        }
        if (ch > 255) {
            return "'" + c + "' (code " + ch + " / 0x" + Integer.toHexString(ch) + ")";
        }
        return "'" + c + "' (code " + ch + ")";
    }

    private void _throwInternal() {
        throw new IllegalStateException("Internal error: code path should never get executed");
    }

    /**
     * Method for reporting low-level decoding (parsing) problems
     */
    protected final void _reportError(String msg) throws StreamReadException {
        throw new StreamReadException(_owner, msg);
    }
}