# ScratchFileBuffer.java

    /*
     * Licensed to the Apache Software Foundation (ASF) under one or more
     * contributor license agreements.  See the NOTICE file distributed with
     * this work for additional information regarding copyright ownership.
     * The ASF licenses this file to You 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 org.apache.pdfbox.io;

    import java.io.EOFException;
    import java.io.IOException;

    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;

    /**
     * Implementation of {@link RandomAccess} as sequence of multiple fixed size pages handled
     * by {@link ScratchFile}.
     */
    class ScratchFileBuffer implements RandomAccess
    {
        private final int pageSize;
        /**
         * The underlying page handler.
         */
        private ScratchFile pageHandler;
        /**
         * The number of bytes of content in this buffer.
         */
        private long size = 0;
        /**
         * Index of current page in {@link #pageIndexes} (the nth page within this buffer).
         */
        private int currentPagePositionInPageIndexes;
        /**
         * The offset of the current page within this buffer.
         */
        private long currentPageOffset;
        /**
         * The current page data.
         */
        private byte[] currentPage;
        /**
         * The current position (for next read/write) of the buffer as an offset in the current page.
         */
        private int positionInPage;
        /**
         * <code>true</code> if current page was changed by a write method
         */
        private boolean currentPageContentChanged = false;

        /** contains ordered list of pages with the index the page is known by page handler ({@link ScratchFile}) */
        private int[] pageIndexes = new int[16];
        /** number of pages held by this buffer */
        private int pageCount = 0;

        private static final Log LOG = LogFactory.getLog(ScratchFileBuffer.class);

        /**
         * Creates a new buffer using pages handled by provided {@link ScratchFile}.
         *
         * @param pageHandler The {@link ScratchFile} managing the pages to be used by this buffer.
         *
         * @throws IOException If getting first page failed.
         */
        ScratchFileBuffer(ScratchFile pageHandler) throws IOException
        {
            pageHandler.checkClosed();

            this.pageHandler = pageHandler;

            pageSize = this.pageHandler.getPageSize();

            addPage();
        }

        /**
         * Checks if this buffer, or the underlying {@link ScratchFile} have been closed,
         * throwing {@link IOException} if so.
         *
         * @throws IOException If either this buffer, or the underlying {@link ScratchFile} have been closed.
         */
        private void checkClosed() throws IOException
        {
            if (pageHandler == null)
            {
                throw new IOException("Buffer already closed");
            }
            pageHandler.checkClosed();
        }

        /**
         * Adds a new page and positions all pointers to start of new page.
         *
         * @throws IOException if requesting a new page fails
         */
        private void addPage() throws IOException
        {
            if (pageCount+1 >= pageIndexes.length)
            {
                int newSize = pageIndexes.length*2;
                // check overflow
                if (newSize<pageIndexes.length)
                {
                    if (pageIndexes.length == Integer.MAX_VALUE)
                    {
                        throw new IOException("Maximum buffer size reached.");
                    }
                    newSize = Integer.MAX_VALUE;
                }
                int[] newPageIndexes = new int[newSize];
                System.arraycopy(pageIndexes, 0, newPageIndexes, 0, pageCount);
                pageIndexes = newPageIndexes;
            }

            int newPageIdx = pageHandler.getNewPage();

            pageIndexes[pageCount] = newPageIdx;
            currentPagePositionInPageIndexes = pageCount;
            currentPageOffset = ((long)pageCount) * pageSize;
            pageCount++;
            currentPage = new byte[pageSize];
            positionInPage = 0;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public long length() throws IOException
        {
            return size;
        }

        /**
         * Ensures the current page has at least one byte left
         * ({@link #positionInPage} in &lt; {@link #pageSize}).
         *
         * <p>If this is not the case we go to next page (writing
         * current one if changed). If current buffer has no more
         * pages we add a new one.</p>
         *
         * @param addNewPageIfNeeded if <code>true</code> it is allowed to add a new page in case
         *                           we are currently at end of last buffer page
         *
         * @return <code>true</code> if we were successful positioning pointer before end of page;
         *         we might return <code>false</code> if it is not allowed to add another page
         *         and current pointer points at end of last page
         *
         * @throws IOException
         */
        private boolean ensureAvailableBytesInPage(boolean addNewPageIfNeeded) throws IOException
        {
            if (positionInPage >= pageSize)
            {
                // page full
                if (currentPageContentChanged)
                {
                    // write page
                    pageHandler.writePage(pageIndexes[currentPagePositionInPageIndexes], currentPage);
                    currentPageContentChanged = false;
                }
                // get new page
                if (currentPagePositionInPageIndexes+1 < pageCount)
                {
                    // we already have more pages assigned (there was a backward seek before)
                    currentPage = pageHandler.readPage(pageIndexes[++currentPagePositionInPageIndexes]);
                    currentPageOffset = ((long)currentPagePositionInPageIndexes) * pageSize;
                    positionInPage = 0;
                }
                else if (addNewPageIfNeeded)
                {
                    // need new page
                    addPage();
                }
                else
                {
                    // we are at last page and are not allowed to add new page
                    return false;
                }
            }
            return true;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void write(int b) throws IOException
        {
            checkClosed();

            ensureAvailableBytesInPage(true);

            currentPage[positionInPage++] = (byte) b;
            currentPageContentChanged = true;

            if(currentPageOffset + positionInPage > size)
            {
                size = currentPageOffset + positionInPage;
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void write(byte[] b) throws IOException
        {
            write(b, 0, b.length);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void write(byte[] b, int off, int len) throws IOException
        {
            checkClosed();

            int remain = len;
            int bOff   = off;

            while (remain > 0)
            {
                ensureAvailableBytesInPage(true);

                int bytesToWrite = Math.min(remain, pageSize-positionInPage);

                System.arraycopy(b, bOff, currentPage, positionInPage, bytesToWrite);

                positionInPage += bytesToWrite;
                currentPageContentChanged = true;

                bOff   += bytesToWrite;
                remain -= bytesToWrite;
            }

            if(currentPageOffset + positionInPage > size)
            {
                size = currentPageOffset + positionInPage;
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public final void clear() throws IOException
        {
            checkClosed();

            // keep only the first page, discard all other pages
            pageHandler.markPagesAsFree(pageIndexes, 1, pageCount - 1);
            pageCount = 1;

            // change to first page if we are not already there
            if (currentPagePositionInPageIndexes > 0)
            {
                currentPage = pageHandler.readPage(pageIndexes[0]);
                currentPagePositionInPageIndexes = 0;
                currentPageOffset = 0;
            }
            positionInPage = 0;
            size = 0;
            currentPageContentChanged = false;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public long getPosition() throws IOException
        {
            checkClosed();
            return currentPageOffset + positionInPage;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void seek(long seekToPosition) throws IOException
        {
            checkClosed();

            /*
             * for now we won't allow to seek past end of buffer; this can be changed by adding new pages as needed
             */
            if (seekToPosition > size)
            {
                throw new EOFException();
            }

            if (seekToPosition < 0)
            {
                throw new IOException("Negative seek offset: " + seekToPosition);
            }

            if ((seekToPosition >= currentPageOffset) && (seekToPosition <= currentPageOffset + pageSize))
            {
                // within same page
                positionInPage = (int) (seekToPosition - currentPageOffset);
            }
            else
            {
                // have to go to another page

                // check if current page needs to be written to file
                if (currentPageContentChanged)
                {
                    pageHandler.writePage(pageIndexes[currentPagePositionInPageIndexes], currentPage);
                    currentPageContentChanged = false;
                }

                int newPagePosition = (int) (seekToPosition / pageSize);
                if (seekToPosition % pageSize == 0 && seekToPosition == size)
                {
                    newPagePosition--; // PDFBOX-4756: Prevent seeking a non-yet-existent page...
                }

                currentPage = pageHandler.readPage(pageIndexes[newPagePosition]);
                currentPagePositionInPageIndexes = newPagePosition;
                currentPageOffset = ((long)currentPagePositionInPageIndexes) * pageSize;
                positionInPage = (int) (seekToPosition - currentPageOffset);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean isClosed()
        {
            return pageHandler == null;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean isEOF() throws IOException
        {
            checkClosed();
            return currentPageOffset + positionInPage >= size;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public int read() throws IOException
        {
            checkClosed();

            if (currentPageOffset + positionInPage >= size)
            {
                return -1;
            }

            if (! ensureAvailableBytesInPage(false))
            {
                // should not happen, we checked it before
                throw new IOException("Unexpectedly no bytes available for read in buffer.");
            }

            return currentPage[positionInPage++] & 0xff;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public int read(byte[] b, int off, int len) throws IOException
        {
            checkClosed();

            if (currentPageOffset + positionInPage >= size)
            {
                return -1;
            }

            int remain = (int) Math.min(len, size - (currentPageOffset + positionInPage));

            int totalBytesRead = 0;
            int bOff           = off;

            while (remain > 0)
            {
                if (! ensureAvailableBytesInPage(false))
                {
                    // should not happen, we checked it before
                    throw new IOException("Unexpectedly no bytes available for read in buffer.");
                }

                int readBytes = Math.min(remain, pageSize - positionInPage);

                System.arraycopy(currentPage, positionInPage, b, bOff, readBytes);

                positionInPage += readBytes;
                totalBytesRead += readBytes;
                bOff += readBytes;
                remain -= readBytes;
            }

            return totalBytesRead;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void close() throws IOException
        {
            if (pageHandler != null) {

                pageHandler.markPagesAsFree(pageIndexes, 0, pageCount);
                pageHandler = null;

                pageIndexes = null;
                currentPage = null;
                currentPageOffset = 0;
                currentPagePositionInPageIndexes = -1;
                positionInPage = 0;
                size = 0;
            }
        }

        /**
         * While calling finalize is normally discouraged we will have to
         * use it here as long as closing a scratch file buffer is not
         * done in every case. Currently {@link COSStream} creates new
         * buffers without closing the old one - which might still be
         * used.
         *
         * <p>Enabling debugging one will see if there are still cases
         * where the buffer is not closed.</p>
         */
        @Override
        protected void finalize() throws Throwable
        {
            try
            {
                if ((pageHandler != null) && LOG.isDebugEnabled())
                {
                    LOG.debug("ScratchFileBuffer not closed!");
                }
                close();
            }
            finally
            {
                super.finalize();
            }
        }

        @Override
        public RandomAccessReadView createView(long startPosition, long streamLength) throws IOException
        {
            return new RandomAccessReadView(this, startPosition, streamLength);
        }

    }