CSVReaderUTF8.java

package com.alibaba.fastjson2.support.csv;

import com.alibaba.fastjson2.JSONException;
import com.alibaba.fastjson2.JSONFactory;
import com.alibaba.fastjson2.reader.*;
import com.alibaba.fastjson2.stream.StreamReader;
import com.alibaba.fastjson2.util.DateUtils;
import com.alibaba.fastjson2.util.Fnv;
import com.alibaba.fastjson2.util.IOUtils;
import com.alibaba.fastjson2.util.TypeUtils;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Function;

import static com.alibaba.fastjson2.util.DateUtils.DEFAULT_ZONE_ID;

final class CSVReaderUTF8<T>
        extends CSVReader<T> {
    static final int ESCAPE_INDEX_NOT_SET = -2;
    int nextEscapeIndex = ESCAPE_INDEX_NOT_SET;

    static final Map<Long, Function<Consumer, ByteArrayValueConsumer>> valueConsumerCreators
            = new ConcurrentHashMap<>();

    byte[] buf;
    InputStream input;
    Charset charset = StandardCharsets.UTF_8;
    ByteArrayValueConsumer valueConsumer;

    CSVReaderUTF8(Feature... features) {
        for (Feature feature : features) {
            this.features |= feature.mask;
        }
    }

    CSVReaderUTF8(byte[] bytes, int off, int len, Charset charset, Class<T> objectClass) {
        super(objectClass);
        this.buf = bytes;
        this.off = off;
        this.end = off + len;
        this.charset = charset;
    }

    CSVReaderUTF8(byte[] bytes, int off, int len, Charset charset, ByteArrayValueConsumer valueConsumer) {
        this.valueConsumer = valueConsumer;
        this.buf = bytes;
        this.off = off;
        this.end = off + len;
        this.charset = charset;
    }

    CSVReaderUTF8(byte[] bytes, int off, int len, Type[] types) {
        super(types);
        this.buf = bytes;
        this.off = off;
        this.end = off + len;
        this.types = types;
    }

    CSVReaderUTF8(byte[] bytes, int off, int len, Class<T> objectClass) {
        super(objectClass);
        this.buf = bytes;
        this.off = off;
        this.end = off + len;
    }

    CSVReaderUTF8(InputStream input, Charset charset, Type[] types) {
        super(types);
        this.charset = charset;
        this.input = input;
    }

    CSVReaderUTF8(InputStream input, Charset charset, Class<T> objectClass) {
        super(objectClass);
        this.charset = charset;
        this.input = input;
    }

    CSVReaderUTF8(InputStream input, Charset charset, ByteArrayValueConsumer valueConsumer) {
        this.charset = charset;
        this.input = input;
        this.valueConsumer = valueConsumer;
    }

    static boolean containsQuoteOrLineSeparator(long v) {
        /*
          for (int i = 0; i < 8; ++i) {
            byte c = (byte) v;
            if (c == '"' || c == '\n' || c == '\r') {
                return true;
            }
            v >>>= 8;
          }
          return false;
         */
        long x22 = v ^ 0x2222222222222222L; // " -> 0x22
        long x0a = v ^ 0x0A0A0A0A0A0A0A0AL; // \n -> 0x0a
        long x0d = v ^ 0x0D0D0D0D0D0D0D0DL; // \r -> 0x0d
        x22 = (x22 - 0x0101010101010101L) & ~x22;
        x0a = (x0a - 0x0101010101010101L) & ~x0a;
        x0d = (x0d - 0x0101010101010101L) & ~x0d;
        return ((x22 | x0a | x0d) & 0x8080808080808080L) != 0;
    }

    private byte[] getBuf() throws IOException {
        byte[] buf = this.buf;
        if (buf != null) {
            return buf;
        }
        return initBuf();
    }

    private byte[] initBuf() throws IOException {
        if (input == null) {
            throw new JSONException("init buf error");
        }
        byte[] buf = new byte[SIZE_512K];
        int end = 0;
        while (end < buf.length) {
            int cnt = input.read(buf, off, buf.length - off);
            if (cnt == -1) {
                inputEnd = true;
                break;
            }
            end += cnt;
        }
        this.end = end;
        if (end > 4 && IOUtils.isUTF8BOM(buf, 0)) {
            off = 3;
            lineNextStart = 3;
        }
        this.buf = buf;
        return buf;
    }

    protected boolean seekLine() throws IOException {
        byte[] buf = getBuf();
        int off = this.off, end = this.end;

        int escapeIndex = this.nextEscapeIndex;
        if (escapeIndex == ESCAPE_INDEX_NOT_SET || (escapeIndex != -1 && escapeIndex < off)) {
            nextEscapeIndex = escapeIndex = IOUtils.indexOfDoubleQuote(buf, off, end);
        }

        int lineSeparatorIndex = IOUtils.indexOfLineSeparator(buf, off, end);
        if (lineSeparatorIndex == -1) {
            if (input != null) {
                if (!inputEnd) {
                    int rest = end - off;
                    if (rest > 0) {
                        System.arraycopy(buf, off, buf, 0, rest);
                    }
                    end = rest;

                    while (end < buf.length) {
                        int cnt = input.read(buf, end, buf.length - end);
                        if (cnt != -1) {
                            end += cnt;
                        } else {
                            inputEnd = true;
                            break;
                        }
                    }
                    this.nextEscapeIndex = escapeIndex >= off
                            ? escapeIndex - off
                            : IOUtils.indexOfDoubleQuote(buf, rest, end);
                    lineSeparatorIndex = IOUtils.indexOfLineSeparator(buf, rest, end);
                    this.off = off = 0;
                    this.end = end;
                }
            }
            if (lineSeparatorIndex == -1 && inputEnd) {
                if (off < end) {
                    lineSeparatorIndex = end;
                } else {
                    return false;
                }
            }
        }
        if ((lineSeparatorIndex != -1) && (escapeIndex == -1 || escapeIndex > lineSeparatorIndex)) {
            this.lineTerminated = true;
            this.lineStart = off;
            this.lineEnd = lineSeparatorIndex != off && buf[lineSeparatorIndex - 1] == '\r'
                    ? lineSeparatorIndex - 1
                    : lineSeparatorIndex;
            this.off = lineSeparatorIndex + 1;
            return true;
        }

        return seekLine0(buf, off, end);
    }

    private boolean seekLine0(byte[] buf, int off, int end) throws IOException {
        int lineNextStart = off;
        for (int k = 0; k < 3; ++k) {
            lineTerminated = false;
            for (int i = off; i < end; i++) {
                byte ch = buf[i];
                if (ch == '"') {
                    lineSize++;
                    if (!quote) {
                        quote = true;
                    } else {
                        int n = i + 1;
                        if (n >= end) {
                            break;
                        }
                        if (buf[n] == '"') {
                            lineSize++;
                            i++;
                        } else {
                            quote = false;
                        }
                    }
                    continue;
                }

                if (quote) {
                    lineSize++;
                    continue;
                }

                if (ch == '\r' || ch == '\n') {
                    if (lineSize > 0 || (features & Feature.IgnoreEmptyLine.mask) == 0) {
                        rowCount++;
                    }

                    lineTerminated = true;
                    lineSize = 0;
                    lineEnd = i;

                    if (ch == '\r') {
                        int n = i + 1;
                        if (n >= end) {
                            break;
                        }
                        if (buf[n] == '\n') {
                            i++;
                        }
                    }

                    lineStart = lineNextStart;
                    lineNextStart = off = i + 1;

                    break;
                } else {
                    lineSize++;
                }
            }

            if (!lineTerminated) {
                if (input != null && !inputEnd) {
                    int len = end - off;
                    int escapedIndex = this.nextEscapeIndex;
                    if (off > 0) {
                        if (len > 0) {
                            System.arraycopy(buf, off, buf, 0, len);
                            this.nextEscapeIndex = escapedIndex >= off ? escapedIndex - off : ESCAPE_INDEX_NOT_SET;
                        }
                        lineStart = lineNextStart = 0;
                        off = 0;
                        end = len;
                        quote = false;
                    }

                    int cnt = input.read(buf, end, buf.length - end);
                    if (cnt == -1) {
                        inputEnd = true;
                        if (off == end) {
                            this.off = off;
                            return false;
                        }
                    } else {
                        end += cnt;
                        this.end = end;
                        continue;
                    }
                }

                lineStart = lineNextStart;
                lineEnd = end;
                rowCount++;
                lineSize = 0;
                off = end;
            }

            lineTerminated = off == end;
            break;
        }

        this.off = off;
        return true;
    }

    Object readValue(byte[] bytes, int off, int len, Type type) {
        if (len == 0) {
            return null;
        }

        if (type == Integer.class) {
            return TypeUtils.parseInt(bytes, off, len);
        }

        if (type == Long.class) {
            return TypeUtils.parseLong(bytes, off, len);
        }

        if (type == BigDecimal.class) {
            return TypeUtils.parseBigDecimal(bytes, off, len);
        }

        if (type == Float.class) {
            return TypeUtils.parseFloat(bytes, off, len);
        }

        if (type == Double.class) {
            return TypeUtils.parseDouble(bytes, off, len);
        }

        if (type == Date.class) {
            long millis = DateUtils.parseMillis(bytes, off, len, charset, DEFAULT_ZONE_ID);
            return new Date(millis);
        }

        if (type == Boolean.class) {
            return TypeUtils.parseBoolean(bytes, off, len);
        }

        String str = new String(bytes, off, len, charset);
        return TypeUtils.cast(str, type);
    }

    public boolean isEnd() {
        return inputEnd;
    }

    public Object[] readLineValues(boolean strings) {
        try {
            if (inputEnd && off == end) {
                return null;
            }

            if (input == null) {
                if (off >= end) {
                    return null;
                }
            }

            boolean result = seekLine();

            if (!result) {
                return null;
            }
        } catch (IOException e) {
            throw new JSONException("seekLine error", e);
        }

        int escapedIndex = this.nextEscapeIndex;
        return escapedIndex == -1 || escapedIndex >= lineEnd
                ? readLineValue(strings)
                : readLineValueEscaped(strings);
    }

    private Object[] readLineValue(boolean strings) {
        List<String> columns = this.columns;
        Object[] values = null;
        List<Object> valueList = null;
        if (columns != null) {
            int size = columns.size();
            values = strings ? new String[size] : new Object[size];
        } else {
            valueList = new ArrayList<>();
        }

        int valueStart = lineStart, lineEnd = this.lineEnd;
        int valueSize = 0;
        int columnIndex = 0;
        byte[] buf = this.buf;
        for (int i = lineStart; i < lineEnd; ++i) {
            if (buf[i] == ',') {
                readValue(strings, columnIndex, valueSize, buf, valueStart, values, valueList);
                valueStart = i + 1;
                valueSize = 0;
                columnIndex++;
                continue;
            }

            valueSize++;
        }

        if (valueSize > 0) {
            readValue(strings, columnIndex, valueSize, buf, valueStart, values, valueList);
        }

        if (values == null) {
            if (valueList != null) {
                int size = valueList.size();
                valueList.toArray(
                        values = strings ? new String[size] : new Object[size]
                );
            }
        }

        if (input == null && off == end) {
            inputEnd = true;
        }

        return values;
    }

    private void readValue(boolean strings,
                           int columnIndex,
                           int valueSize,
                           byte[] buf,
                           int valueStart,
                           Object[] values,
                           List<Object> valueList) {
        Type type = types != null && columnIndex < types.length ? types[columnIndex] : null;

        Object value;
        if (type == null || type == String.class || type == Object.class || strings) {
            byte c0, c1;
            if (valueSize == 1 && (c0 = buf[valueStart]) >= 0) {
                value = TypeUtils.toString((char) c0);
            } else if (valueSize == 2
                    && (c0 = buf[valueStart]) >= 0
                    && (c1 = buf[valueStart + 1]) >= 0
            ) {
                value = TypeUtils.toString((char) c0, (char) c1);
            } else {
                value = new String(buf, valueStart, valueSize, charset);
            }
        } else {
            try {
                value = readValue(buf, valueStart, valueSize, type);
            } catch (Exception e) {
                value = error(columnIndex, e);
            }
        }

        if (values != null) {
            if (columnIndex < values.length) {
                values[columnIndex] = value;
            }
        } else {
            valueList.add(value);
        }
    }

    private Object[] readLineValueEscaped(boolean strings) {
        List<String> columns = this.columns;
        Object[] values = null;
        List<Object> valueList = null;
        if (columns != null) {
            int size = columns.size();
            values = strings ? new String[size] : new Object[size];
        }

        boolean quote = false;
        int valueStart = lineStart, lineEnd = this.lineEnd;
        int valueSize = 0;
        int escapeCount = 0;
        int columnIndex = 0;
        byte[] buf = this.buf;
        for (int i = lineStart; i < lineEnd; ++i) {
            byte ch = buf[i];

            if (quote) {
                if (ch == '"') {
                    int n = i + 1;
                    if (n < lineEnd) {
                        byte c1 = buf[n];
                        if (c1 == '"') {
                            valueSize += 2;
                            escapeCount++;
                            ++i;
                            continue;
                        } else if (c1 == ',') {
                            ++i;
                            ch = c1;
                        }
                    } else if (n == lineEnd) {
                        break;
                    }
                } else {
                    valueSize++;
                    continue;
                }
            } else {
                if (ch == '"') {
                    quote = true;
                    continue;
                }
            }

            if (ch == ',') {
                Type type = types != null && columnIndex < types.length ? types[columnIndex] : null;

                Object value;
                if (quote) {
                    if (escapeCount == 0) {
                        if (type == null || type == String.class || type == Object.class || strings) {
                            value = new String(buf, valueStart + 1, valueSize, charset);
                        } else {
                            try {
                                value = readValue(buf, valueStart + 1, valueSize, type);
                            } catch (Exception e) {
                                value = error(columnIndex, e);
                            }
                        }
                    } else {
                        byte[] bytes = new byte[valueSize - escapeCount];
                        int valueEnd = valueStart + valueSize;
                        for (int j = valueStart + 1, k = 0; j < valueEnd; ++j) {
                            byte c = buf[j];
                            bytes[k++] = c;
                            if (c == '"' && buf[j + 1] == '"') {
                                ++j;
                            }
                        }

                        if (type == null || type == String.class || type == Object.class) {
                            value = new String(bytes, charset);
                        } else {
                            try {
                                value = readValue(bytes, 0, bytes.length, type);
                            } catch (Exception e) {
                                value = error(columnIndex, e);
                            }
                        }
                    }
                } else {
                    if (type == null || type == String.class || type == Object.class || strings) {
                        byte c0, c1;
                        if (valueSize == 1 && (c0 = buf[valueStart]) >= 0) {
                            value = TypeUtils.toString((char) c0);
                        } else if (valueSize == 2
                                && (c0 = buf[valueStart]) >= 0
                                && (c1 = buf[valueStart + 1]) >= 0
                        ) {
                            value = TypeUtils.toString((char) c0, (char) c1);
                        } else {
                            value = new String(buf, valueStart, valueSize, charset);
                        }
                    } else {
                        try {
                            value = readValue(buf, valueStart, valueSize, type);
                        } catch (Exception e) {
                            value = error(columnIndex, e);
                        }
                    }
                }

                if (values != null) {
                    if (columnIndex < values.length) {
                        values[columnIndex] = value;
                    }
                } else {
                    if (valueList == null) {
                        valueList = new ArrayList<>();
                    }
                    valueList.add(value);
                }

                quote = false;
                valueStart = i + 1;
                valueSize = 0;
                escapeCount = 0;
                columnIndex++;
                continue;
            }

            valueSize++;
        }

        if (valueSize > 0 || quote) {
            Type type = types != null && columnIndex < types.length ? types[columnIndex] : null;

            Object value;
            if (quote) {
                if (escapeCount == 0) {
                    if (type == null || type == String.class || type == Object.class || strings) {
                        value = new String(buf, valueStart + 1, valueSize, charset);
                    } else {
                        try {
                            value = readValue(buf, valueStart + 1, valueSize, type);
                        } catch (Exception e) {
                            value = error(columnIndex, e);
                        }
                    }
                } else {
                    byte[] bytes = new byte[valueSize - escapeCount];
                    int valueEnd = lineEnd;
                    for (int j = valueStart + 1, k = 0; j < valueEnd; ++j) {
                        byte c = buf[j];
                        bytes[k++] = c;
                        if (c == '"' && buf[j + 1] == '"') {
                            ++j;
                        }
                    }

                    if (type == null || type == String.class || type == Object.class || strings) {
                        value = new String(bytes, 0, bytes.length, charset);
                    } else {
                        try {
                            value = readValue(bytes, 0, bytes.length, type);
                        } catch (Exception e) {
                            value = error(columnIndex, e);
                        }
                    }
                }
            } else {
                if (type == null || type == String.class || type == Object.class || strings) {
                    byte c0, c1;
                    if (valueSize == 1 && (c0 = buf[valueStart]) >= 0) {
                        value = TypeUtils.toString((char) c0);
                    } else if (valueSize == 2
                            && (c0 = buf[valueStart]) >= 0
                            && (c1 = buf[valueStart + 1]) >= 0
                    ) {
                        value = TypeUtils.toString((char) c0, (char) c1);
                    } else {
                        value = new String(buf, valueStart, valueSize, charset);
                    }
                } else {
                    try {
                        value = readValue(buf, valueStart, valueSize, type);
                    } catch (Exception e) {
                        value = error(columnIndex, e);
                    }
                }
            }

            if (values != null) {
                if (columnIndex < values.length) {
                    values[columnIndex] = value;
                }
            } else {
                if (valueList == null) {
                    valueList = new ArrayList<>();
                }
                valueList.add(value);
            }
        }

        if (values == null) {
            if (valueList != null) {
                int size = valueList.size();
                valueList.toArray(
                        values = strings ? new String[size] : new Object[size]
                );
            }
        }

        if (input == null && off == end) {
            inputEnd = true;
        }

        return values;
    }

    @Override
    public void close() {
        if (input != null) {
            IOUtils.close(input);
        }
    }

    public void statAll() {
        ByteArrayValueConsumer consumer = (row, column, bytes, off, len, charset) -> {
            StreamReader.ColumnStat stat = getColumnStat(column);
            stat.stat(bytes, off, len, charset);
        };
        readAll(consumer, Integer.MAX_VALUE);
    }

    public void statAll(int maxRows) {
        ByteArrayValueConsumer consumer = (row, column, bytes, off, len, charset) -> {
            StreamReader.ColumnStat stat = getColumnStat(column);
            stat.stat(bytes, off, len, charset);
        };
        readAll(consumer, maxRows);
    }

    public void readAll() {
        if (valueConsumer == null) {
            throw new JSONException("unsupported operation, consumer is null");
        }

        readAll(valueConsumer, Integer.MAX_VALUE);
    }

    public void readAll(int maxRows) {
        if (valueConsumer == null) {
            throw new JSONException("unsupported operation, consumer is null");
        }

        readAll(valueConsumer, maxRows);
    }

    public void readLineObjectAll(boolean readHeader, Consumer<T> consumer) {
        if (readHeader) {
            readHeader();
        }

        if (fieldReaders == null) {
            while (true) {
                Object[] line = readLineValues(false);
                if (line == null) {
                    break;
                }
                consumer.accept((T) line);
            }
            return;
        }

        ObjectReaderProvider provider = JSONFactory.getDefaultObjectReaderProvider();

        // valueConsumerCreators
        if (this.fieldReaders == null) {
            if (objectClass != null) {
                ObjectReaderAdapter objectReader = (ObjectReaderAdapter) provider.getObjectReader(objectClass);
                this.fieldReaders = objectReader.getFieldReaders();
                this.objectCreator = provider.createObjectCreator(objectClass, features);
            }
        }

        Function<Consumer, ByteArrayValueConsumer> valueConsumerCreator;
        String[] strings = new String[this.fieldReaders.length + 1];
        strings[0] = objectClass.getName();
        for (int i = 0; i < this.fieldReaders.length; i++) {
            strings[i + 1] = this.fieldReaders[i].fieldName;
        }
        long fullNameHash = Fnv.hashCode64(strings);
        valueConsumerCreator = valueConsumerCreators.get(fullNameHash);
        if (valueConsumerCreator == null) {
            valueConsumerCreator = provider
                    .createValueConsumerCreator(objectClass, fieldReaders);
            if (valueConsumerCreator != null) {
                valueConsumerCreators.putIfAbsent(fullNameHash, valueConsumerCreator);
            }
        }

        ByteArrayValueConsumer bytesConsumer = null;
        if (valueConsumerCreator != null) {
            bytesConsumer = valueConsumerCreator.apply(consumer);
        }

        if (bytesConsumer == null) {
            bytesConsumer = new ByteArrayConsumerImpl(consumer);
        }

        readAll(bytesConsumer, Integer.MAX_VALUE);
    }

    class ByteArrayConsumerImpl
            implements ByteArrayValueConsumer {
        protected Object object;
        final Consumer consumer;

        public ByteArrayConsumerImpl(Consumer consumer) {
            this.consumer = consumer;
        }

        @Override
        public final void beforeRow(int row) {
            if (objectCreator != null) {
                object = objectCreator.get();
            }
        }

        @Override
        public void accept(int row, int column, byte[] bytes, int off, int len, Charset charset) {
            if (column >= fieldReaders.length || len == 0) {
                return;
            }

            FieldReader fieldReader = fieldReaders[column];
            Object fieldValue = readValue(bytes, off, len, fieldReader.fieldType);
            fieldReader.accept(object, fieldValue);
        }

        @Override
        public final void afterRow(int row) {
            consumer.accept(object);
            object = null;
        }
    }

    private void readAll(ByteArrayValueConsumer consumer, int maxRows) {
        consumer.start();

        for (int r = 0; r < maxRows || maxRows < 0; ++r) {
            try {
                if (inputEnd & off == end) {
                    break;
                }

                if (input == null) {
                    if (off >= end) {
                        break;
                    }
                }

                boolean result = seekLine();

                if (!result) {
                    break;
                }
            } catch (IOException e) {
                throw new JSONException("seekLine error", e);
            }

            consumer.beforeRow(rowCount);

            int escapedIndex = this.nextEscapeIndex;
            if (escapedIndex == -1 || escapedIndex >= lineEnd) {
                readLine(consumer);
            } else {
                readLineEscaped(consumer);
            }

            consumer.afterRow(rowCount);
        }
        consumer.end();
    }

    private void readLine(ByteArrayValueConsumer consumer) {
        int valueStart = lineStart;
        int valueSize = 0;
        int columnIndex = 0;
        byte[] buf = this.buf;
        for (int i = lineStart; i < lineEnd; ++i) {
            if (buf[i] == ',') {
                consumer.accept(rowCount, columnIndex, buf, valueStart, valueSize, charset);
                valueStart = i + 1;
                valueSize = 0;
                columnIndex++;
                continue;
            }

            valueSize++;
        }

        if (valueSize > 0) {
            consumer.accept(rowCount, columnIndex, buf, valueStart, valueSize, charset);
        }
    }

    private void readLineEscaped(ByteArrayValueConsumer consumer) {
        boolean quote = false;
        int valueStart = lineStart;
        int valueSize = 0;
        int escapeCount = 0;
        int columnIndex = 0;
        for (int i = lineStart; i < lineEnd; ++i) {
            byte ch = buf[i];

            if (quote) {
                if (ch == '"') {
                    int n = i + 1;
                    if (n < lineEnd) {
                        byte c1 = buf[n];
                        if (c1 == '"') {
                            valueSize += 2;
                            escapeCount++;
                            ++i;
                            continue;
                        } else if (c1 == ',') {
                            ++i;
                            ch = c1;
                        }
                    } else if (n == lineEnd) {
                        break;
                    }
                } else {
                    valueSize++;
                    continue;
                }
            } else {
                if (ch == '"') {
                    quote = true;
                    continue;
                }
            }

            if (ch == ',') {
                byte[] columnBuf = buf;
                int columnStart = 0;
                int columnSize = valueSize;
                if (quote) {
                    if (escapeCount == 0) {
                        columnStart = valueStart + 1;
                    } else {
                        byte[] bytes = new byte[valueSize - escapeCount];
                        int valueEnd = valueStart + valueSize;
                        for (int j = valueStart + 1, k = 0; j < valueEnd; ++j) {
                            byte c = buf[j];
                            bytes[k++] = c;
                            if (c == '"' && buf[j + 1] == '"') {
                                ++j;
                            }
                        }

                        columnBuf = bytes;
                        columnSize = bytes.length;
                    }
                } else {
                    columnStart = valueStart;
                }
                consumer.accept(rowCount, columnIndex, columnBuf, columnStart, columnSize, charset);

                quote = false;
                valueStart = i + 1;
                valueSize = 0;
                escapeCount = 0;
                columnIndex++;
                continue;
            }

            valueSize++;
        }

        if (valueSize > 0) {
            byte[] columnBuf = buf;
            int columnStart = 0;
            int columnSize = valueSize;
            if (quote) {
                if (escapeCount == 0) {
                    columnStart = valueStart + 1;
                } else {
                    byte[] bytes = new byte[valueSize - escapeCount];
                    int valueEnd = lineEnd;
                    for (int j = valueStart + 1, k = 0; j < valueEnd; ++j) {
                        byte c = buf[j];
                        bytes[k++] = c;
                        if (c == '"' && buf[j + 1] == '"') {
                            ++j;
                        }
                    }

                    columnBuf = bytes;
                    columnSize = bytes.length;
                }
            } else {
                columnStart = valueStart;
            }
            consumer.accept(rowCount, columnIndex, columnBuf, columnStart, columnSize, charset);
        }
    }
}