SimpleStreamReader.java

package org.jsoup.internal;

import org.jsoup.helper.Validate;
import org.jspecify.annotations.Nullable;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;

import static org.jsoup.internal.SimpleBufferedInput.BufferPool;

/**
 A simple decoding InputStreamReader that recycles internal buffers.
 */
public class SimpleStreamReader extends Reader {
    private final InputStream in;
    private final CharsetDecoder decoder;
    private @Nullable ByteBuffer byteBuf; // null after close

    public SimpleStreamReader(InputStream in, Charset charset) {
        this.in = in;
        this.decoder = charset.newDecoder()
            .onMalformedInput(CodingErrorAction.REPLACE)
            .onUnmappableCharacter(CodingErrorAction.REPLACE);
        byte[] buf = BufferPool.borrow(); // shared w/ SimpleBufferedInput, ControllableInput
        byteBuf = ByteBuffer.wrap(buf);
        byteBuf.flip(); // limit(0)
    }

    @Override
    public int read(char[] charArray, int off, int len) throws IOException {
        Validate.notNull(byteBuf); // can't read after close
        CharBuffer charBuf = CharBuffer.wrap(charArray, off, len);
        if (charBuf.position() != 0) charBuf = charBuf.slice();

        boolean readFully = false;
        while (true) {
            CoderResult result = decoder.decode(byteBuf, charBuf, readFully);
            if (result.isUnderflow()) {
                if (readFully || !charBuf.hasRemaining() || (charBuf.position() > 0) && !(in.available() > 0))
                    break;
                int read = bufferUp();
                if (read < 0) {
                    readFully = true;
                    if ((charBuf.position() == 0) && (!byteBuf.hasRemaining()))
                        break;
                }
                continue;
            }
            if (result.isOverflow()) break;
            result.throwException();
        }

        if (readFully) decoder.reset();
        if (charBuf.position() == 0) {
            return readFully ? -1 : 0; // 0 if there was a surrogate and reader tried to read only 1.
        }
        return charBuf.position();
    }

    private int bufferUp() throws IOException {
        assert byteBuf != null; // already validated ^
        byteBuf.compact();
        try {
            int pos = byteBuf.position();
            int remaining = (byteBuf.limit() - pos);
            int read = in.read(byteBuf.array(), byteBuf.arrayOffset() + pos, remaining);
            if (read < 0) return read;
            if (read == 0) throw new IOException("Underlying input stream returned zero bytes");
            byteBuf.position(pos + read);
        } finally {
            byteBuf.flip();
        }
        return byteBuf.remaining();
    }

    @Override
    public void close() throws IOException {
        if (byteBuf == null) return;
        BufferPool.release(byteBuf.array());
        byteBuf = null;
        in.close();
    }
}