SimpleBufferedInput.java

package org.jsoup.internal;

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

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;

import static org.jsoup.internal.SharedConstants.DefaultBufferSize;

/**
 A simple implementation of a buffered input stream, in which we can control the byte[] buffer to recycle it. Not safe for
 use between threads; no sync or locks. The buffer is borrowed on initial demand in fill.
 @since 1.18.2
 */
class SimpleBufferedInput extends FilterInputStream {
    static final int BufferSize = DefaultBufferSize;
    static final SoftPool<byte[]> BufferPool = new SoftPool<>(() -> new byte[BufferSize]);
    private int capRemaining = Integer.MAX_VALUE; // how many bytes we are allowed to pull from the underlying stream

    private byte @Nullable [] byteBuf; // the byte buffer; recycled via SoftPool. Created in fill if required
    private int bufPos;
    private int bufLength;
    private int bufMark = -1; // mark set by ControllableInputStream; -1 when unset
    private boolean inReadFully = false; // true when the underlying inputstream has been read fully

    SimpleBufferedInput(@Nullable InputStream in) {
        super(in);
        if (in == null) inReadFully = true; // effectively an empty stream
    }

    @Override
    public int read() throws IOException {
        if (bufPos >= bufLength) {
            fill();
            if (bufPos >= bufLength)
                return -1;
        }
        return getBuf()[bufPos++] & 0xff;
    }

    @Override
    public int read(byte[] dest, int offset, int desiredLen) throws IOException {
        Validate.notNull(dest);
        if (offset < 0 || desiredLen < 0 || desiredLen > dest.length - offset) {
            throw new IndexOutOfBoundsException();
        } else if (desiredLen == 0) {
            return 0;
        }

        int bufAvail = bufLength - bufPos;
        if (bufAvail <= 0) { // can't serve from the buffer
            fill();
            bufAvail = bufLength - bufPos;
        }

        int read = Math.min(bufAvail, desiredLen);
        if (read <= 0) {
            return -1;
        }

        System.arraycopy(getBuf(), bufPos, dest, offset, read);
        bufPos += read;
        return read;
    }

    private void fill() throws IOException {
        if (inReadFully) return;
        if (byteBuf == null) { // get one on first demand
            byteBuf = BufferPool.borrow();
        }

        compact();
        bufLength = bufPos;
        int toRead = Math.min(byteBuf.length - bufPos, capRemaining);
        if (toRead <= 0) return;
        int read = in.read(byteBuf, bufPos, toRead);
        if (read > 0) {
            bufLength = read + bufPos;
            capRemaining -= read;
            while (byteBuf.length - bufLength > 0 && capRemaining > 0) { // read in more if we have space, without blocking
                if (in.available() < 1) break;
                toRead = Math.min(byteBuf.length - bufLength, capRemaining);
                if (toRead <= 0) break;
                read = in.read(byteBuf, bufLength, toRead);
                if (read <= 0) break;
                bufLength += read;
                capRemaining -= read;
            }
        }
        if (read == -1) inReadFully = true;
    }

    byte[] getBuf() {
        Validate.notNull(byteBuf);
        return byteBuf;
    }

    /**
     Check if the underlying InputStream has been read fully. There may still content in this buffer to be consumed.
     @return true if the underlying inputstream has been read fully.
     */
    boolean baseReadFully() {
        return inReadFully;
    }

    void resetFullyRead() {
        if (in != null) // for null-wrapped streams, leave as fully read to avoid fill() on a null input
            inReadFully = false;
    }

    @Override
    public int available() throws IOException {
        int buffered = (byteBuf != null) ? (bufLength - bufPos) : 0;
        if (buffered > 0) {
            return buffered; // doesn't include those in.available(), but mostly used as a block test
        }
        int avail = inReadFully ? 0 : in.available();
        return avail;
    }

    void capRemaining(int newRemaining) {
        capRemaining = Math.max(0, newRemaining);
    }

    void setMark() {
        bufMark = bufPos;
    }

    void rewindToMark() throws IOException {
        if (bufMark < 0)
            throw new IOException("Resetting to invalid mark");
        bufPos = bufMark;
    }

    void clearMark() {
        bufMark = -1;
    }

    private void compact() {
        if (byteBuf == null || bufPos == 0) return;
        int keepFrom = bufMark >= 0 ? bufMark : bufPos;
        if (keepFrom <= 0) return;

        int remaining = bufLength - keepFrom;
        if (remaining > 0) {
            System.arraycopy(byteBuf, keepFrom, byteBuf, 0, remaining);
        }
        bufLength = remaining;
        bufPos -= keepFrom;
        if (bufMark >= 0) {
            bufMark -= keepFrom;
        }
    }

    @Override
    public void close() throws IOException {
        if (in != null) super.close();
        if (byteBuf == null) return; // already closed, or never allocated
        BufferPool.release(byteBuf); // return the buffer to the pool
        byteBuf = null; // NPE further attempts to read
    }
}