GordianInputStream.java

/*
 * GordianKnot: Security Suite
 * Copyright 2012-2026. Tony Washer
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License.  You may obtain a copy
 * of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package io.github.tonywasher.joceanus.gordianknot.impl.core.stream;

import io.github.tonywasher.joceanus.gordianknot.api.base.GordianException;
import io.github.tonywasher.joceanus.gordianknot.impl.core.base.GordianDataConverter;
import org.bouncycastle.util.Arrays;

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

/**
 * Input stream base implementation.
 */
abstract class GordianInputStream
        extends InputStream {
    /**
     * Closed stream failure.
     */
    static final String ERROR_CLOSED = "Stream is closed";

    /**
     * Buffer size for transfers.
     */
    static final int BUFSIZE = 1024;

    /**
     * Byte mask.
     */
    static final int BYTE_MASK = GordianDataConverter.BYTE_MASK;

    /**
     * The underlying input stream.
     */
    private final InputStream theStream;

    /**
     * The holding buffer for data that has been processed but not read.
     */
    private GordianProcessedBuffer theProcessed;

    /**
     * The buffer used for reading from input stream.
     */
    private final byte[] theBuffer = new byte[BUFSIZE];

    /**
     * The single byte buffer.
     */
    private final byte[] theByteBuffer = new byte[1];

    /**
     * has this stream been closed.
     */
    private boolean isClosed;

    /**
     * Constructor.
     *
     * @param pInput the underlying input stream
     */
    GordianInputStream(final InputStream pInput) {
        theStream = pInput;
    }

    /**
     * Set the processed buffer.
     *
     * @param pProcessed the processed buffer
     */
    void setProcessedBuffer(final GordianProcessedBuffer pProcessed) {
        theProcessed = pProcessed;
    }

    @Override
    public void close() throws IOException {
        /* Null operation if we are already closed */
        if (!isClosed) {
            /* Close the input stream */
            theStream.close();
            isClosed = true;

            /* Clear the buffers */
            Arrays.fill(theBuffer, (byte) 0);
            theProcessed.setBuffer(null, 0);
        }
    }

    @Override
    public long skip(final long pNumToSkip) throws IOException {
        /* If we are already closed throw IO Exception */
        if (isClosed) {
            throw new IOException(ERROR_CLOSED);
        }

        /* Loop while skipping bytes */
        long myNumToSkip = pNumToSkip;
        long myTotalSkipped = 0;
        while (myNumToSkip > 0) {
            /* Skip processed bytes */
            final long myNumSkipped = theProcessed.skipBytes(myNumToSkip);

            /* Adjust counts */
            myNumToSkip -= myNumSkipped;
            myTotalSkipped += myNumSkipped;

            /* If we need further bytes, bring more data in and handle EOF */
            if (myNumToSkip > 0
                    && processMoreData() == -1) {
                return -1;
            }
        }

        /* Return the number of bytes skipped */
        return myTotalSkipped;
    }

    @Override
    public int available() throws IOException {
        /* If we are already closed throw IO Exception */
        if (isClosed) {
            throw new IOException(ERROR_CLOSED);
        }

        /* Determine the number of bytes available */
        return theProcessed.availableInBuffer()
                + theStream.available();
    }

    @Override
    public boolean markSupported() {
        /* return not supported */
        return false;
    }

    @Override
    public synchronized void mark(final int pReadLimit) {
        throw new UnsupportedOperationException();
    }

    @Override
    public synchronized void reset() throws IOException {
        /* If we are already closed then throw IO Exception */
        if (isClosed) {
            throw new IOException(ERROR_CLOSED);
        }

        /* Not supported */
        throw new IOException("Mark is not supported");
    }

    @Override
    public int read(final byte[] pOutBytes) throws IOException {
        /* Read the next bytes from the stream */
        return read(pOutBytes, 0, pOutBytes.length);
    }

    @Override
    public int read() throws IOException {
        int iNumBytesRead;

        /* Loop until we get a byte or EOF from the stream */
        do {
            iNumBytesRead = read(theByteBuffer, 0, 1);
        } while (iNumBytesRead == 0);

        /* Convert the byte that has been read into an integer */
        if (iNumBytesRead > 0) {
            iNumBytesRead = theByteBuffer[0] & BYTE_MASK;
        }

        /* Return to the caller */
        return iNumBytesRead;
    }

    @Override
    public int read(final byte[] pBuffer,
                    final int pOffset,
                    final int pLength) throws IOException {
        /* If we are already closed throw IO Exception */
        if (isClosed) {
            throw new IOException(ERROR_CLOSED);
        }

        /* If there is no data in the processed buffer, bring more data in and handle EOF */
        if (theProcessed.availableInBuffer() == 0
                && processMoreData() == -1) {
            return -1;
        }

        /* Read from the processed buffer and Return the amount of data read */
        return theProcessed.readBytes(pBuffer, pOffset, pLength);
    }

    /**
     * Process more data from the input stream.
     *
     * @return number of bytes read or -1 if EOF
     * @throws IOException on error
     */
    private int processMoreData() throws IOException {
        /* Protect against exceptions */
        try {
            /* If we have already exhausted the source return now */
            if (theProcessed.hasEOFbeenSeen()) {
                return -1;
            }

            /* Read more data from the input stream looping until bytes are available */
            int iNumRead;
            do {
                /* Read more data from the input stream */
                iNumRead = theStream.read(theBuffer, 0, BUFSIZE);

                /* Process any data read */
                if (iNumRead != 0) {
                    /* Process the bytes into the buffer */
                    iNumRead = theProcessed.processBytes(theBuffer, iNumRead);
                }
            } while (iNumRead == 0);

            /* Return number of bytes available */
            return iNumRead;

            /* Catch exceptions */
        } catch (GordianException e) {
            throw new IOException(e);
        }
    }

    /**
     * Buffer to hold the processed data prior to returning it to the caller.
     */
    protected abstract static class GordianProcessedBuffer {
        /**
         * The buffer itself.
         */
        private byte[] theStore;

        /**
         * The length of data in the buffer.
         */
        private int theDataLen;

        /**
         * The read offset of data in the buffer.
         */
        private int theReadOffset;

        /**
         * have we seen EOF.
         */
        private boolean hasEOFbeenSeen;

        /**
         * Determine the amount of data in the buffer.
         *
         * @return the number of data bytes in the buffer
         */
        int availableInBuffer() {
            return theDataLen
                    - theReadOffset;
        }

        /**
         * Has EOF been seen.
         *
         * @return true/false
         */
        boolean hasEOFbeenSeen() {
            return hasEOFbeenSeen;
        }

        /**
         * Set EOF seen marker.
         */
        void setEOFSeen() {
            hasEOFbeenSeen = true;
        }

        /**
         * Set the new buffer.
         *
         * @param pBuffer     the buffer
         * @param pDataLength the dataLength
         */
        void setBuffer(final byte[] pBuffer,
                       final int pDataLength) {
            /* Set new data */
            theStore = pBuffer;
            theDataLen = pDataLength;
            theReadOffset = 0;
        }

        /**
         * Skip bytes.
         *
         * @param pBytesToSkip the number of bytes to skip
         * @return the number of bytes skipped
         */
        long skipBytes(final long pBytesToSkip) {
            /* Determine number of bytes that we can skip */
            final int myAvailable = availableInBuffer();

            /* If we must skip all bytes */
            if (pBytesToSkip >= myAvailable) {
                /* Reset the values */
                theDataLen = 0;
                theReadOffset = 0;
                return myAvailable;

                /* else we only need to skip some bytes */
            } else {
                theReadOffset += pBytesToSkip;
                return pBytesToSkip;
            }
        }

        /**
         * Read a number of bytes out from the buffer.
         *
         * @param pBuffer the buffer to read bytes into
         * @param pOffset the offset at which to start reading bytes
         * @param pLength the maximum length of data to read
         * @return the actual length of data read or -1 if EOF
         */
        int readBytes(final byte[] pBuffer,
                      final int pOffset,
                      final int pLength) {
            /* Determine how much data we have available */
            int iNumRead = theDataLen
                    - theReadOffset;

            /* Determine how much data we can transfer */
            iNumRead = iNumRead <= pLength
                    ? iNumRead
                    : pLength;

            /* If we have data to copy */
            if (iNumRead > 0) {
                /* Transfer the bytes */
                System.arraycopy(theStore, theReadOffset, pBuffer, pOffset, iNumRead);

                /* Adjust ReadOffset */
                theReadOffset += iNumRead;

                /* If we have finished with the data in the buffer */
                if (theReadOffset >= theDataLen) {
                    /* Reset the values */
                    theDataLen = 0;
                    theReadOffset = 0;
                }

                /* else if we have no data check for EOF and report it if required */
            } else if (hasEOFbeenSeen) {
                iNumRead = -1;
            }

            /* Return the number of bytes transferred */
            return iNumRead;
        }

        /**
         * Process bytes into the buffer.
         *
         * @param pBuffer the buffer from which to store bytes
         * @param pLength the number of bytes read into the buffer (must not be zero)
         * @return the number of bytes now available in the buffer
         * @throws GordianException on error
         */
        protected abstract int processBytes(byte[] pBuffer,
                                            int pLength) throws GordianException;
    }
}