ByteArrayIO.java

package nom.tam.util;

/*
 * #%L
 * nom.tam FITS library
 * %%
 * Copyright (C) 1996 - 2021 nom-tam-fits
 * %%
 * This is free and unencumbered software released into the public domain.
 * 
 * Anyone is free to copy, modify, publish, use, compile, sell, or
 * distribute this software, either in source code form or as a compiled
 * binary, for any purpose, commercial or non-commercial, and by any
 * means.
 * 
 * In jurisdictions that recognize copyright laws, the author or authors
 * of this software dedicate any and all copyright interest in the
 * software to the public domain. We make this dedication for the benefit
 * of the public at large and to the detriment of our heirs and
 * successors. We intend this dedication to be an overt act of
 * relinquishment in perpetuity of all present and future rights to this
 * software under copyright law.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 * #L%
 */

import java.io.EOFException;
import java.io.IOException;
import java.util.Arrays;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

/**
 * A class for reading and writing into byte arrays with a stream-like
 * interface.
 * 
 * @author Attila Kovacs
 *
 * @since 1.16
 */
public class ByteArrayIO implements ReadWriteAccess {
    
    /** Mask for 1-byte */
    private static final int BYTE_MASK = 0xFF;
    
    /** The underlying buffer */
    private byte[] buf;
    
    /** Whether the buffer is allowed to grow as needed to contain more data */
    private boolean isGrowable;
    
    /** The current pointer position, for the next read or write in the buffer */
    private int pos;
    
    /** The current end of the buffer, that is the total number of bytes available for reading from the buffer */
    private int end;
   
    /**
     * Instantiates a new byte array with an IO interface, with a fixed-size buffer using the
     * specified array as its backing storage.
     * 
     * @param buffer        the fixed buffer.
     */
    @SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "by design this class provides an IO interface for an accessible array.")
    public ByteArrayIO(byte[] buffer) {
        this.buf = buffer;
        this.end = 0;
        this.isGrowable = false;
    }
    
    /**
     * Instantiates a new byte array with an IO interface, with a growable buffer initialized
     * to the specific size.
     * 
     * @param initialCapacity       the number of bytes to contain in the buffer.
     * @throws IllegalArgumentException
     *                              if the initial capacity is 0 or negative
     */
    public ByteArrayIO(int initialCapacity) throws IllegalArgumentException {
        if (initialCapacity <= 0) {
            throw new IllegalArgumentException("Illegal buffer size:" + initialCapacity);
        }
        
        this.buf = new byte[initialCapacity];
        this.end = 0;
        this.isGrowable = true;
    }
    
    /**
     * Returns a copy of this byte array with an IO interface, including a deep copy of
     * the buffered data.
     * 
     * @return      a deep copy of this byte array with an IO interface instance.
     */
    public synchronized ByteArrayIO copy() {
        ByteArrayIO copy = new ByteArrayIO(Arrays.copyOf(buf, buf.length));
        synchronized (copy) {
            copy.isGrowable = this.isGrowable;
            copy.pos = this.pos;
            copy.end = this.end;
        }
        return copy;
    }
    
    /**
     * Returns the underlying byte array, which is the backing array of this buffer.
     * 
     * @return  the backing array of this buffer.
     */
    @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "by design this class provides an IO interface for an accessible array.")
    public synchronized byte[] getBuffer() {
        return buf;
    }
    
    /**
     * Returns the current capacity of this buffer, that is the total number of
     * bytes that may be written into the current backing buffer.
     * 
     * @return  the current size of the backing array.
     */
    public final synchronized int capacity() {
        return buf.length;
    }
    
    @Override
    public final synchronized long length() {
        return end;
    }
    
    /**
     * Returns the number of bytes available for reading from the current position.
     * 
     * @return  the number of bytes that can be read from this buffer from the current position.
     */
    public final synchronized int getRemaining() {
        if (pos >= end) {
            return 0;
        }
        return end - pos;
    }
    
    @Override
    public final synchronized long position() {
        return pos;
    }
    
    @Override
    public synchronized void position(long offset) throws IOException { 
        if (offset < 0) {
            throw new EOFException("Negative buffer index: " + offset);
        }
        
        if (offset > buf.length) {
            if (!isGrowable) {
                throw new EOFException("Position " + offset + " beyond fixed buffer size " + buf.length);
            }
        }
        
        pos = (int) offset;
    }
    
    /**
     * Changes the length of this buffer. The total number of bytes available
     * from reading from this buffer will be that of the new length. If the buffer
     * is truncated and its pointer is positioned beyond the new size, then the pointer
     * is changed to point to the new buffer end. If the buffer is enlarged beyond
     * it current capacity, its capacity will grow as necessary provided the
     * buffer is growable. Otherwise, an EOFException is thrown if the new length
     * is beyond the fixed buffer capacity. If the new length is larger than the old 
     * one, the added buffer segment may have undefined contents.
     * 
     * @param length        The buffer length, that is number of bytes available for
     *                      reading.
     * @throws IllegalArgumentException 
     *                      if the length is negative or if the new new length 
     *                      exceeds the capacity of a fixed-type buffer.
     *                      
     * @see #length()
     * @see #capacity()
     */
    public synchronized void setLength(int length) throws IllegalArgumentException {
        if (length < 0) {
            throw new IllegalArgumentException("Buffer set to negative length: " + length);
        }
        
        if (length > capacity()) {
            if (!isGrowable) {
                throw new IllegalArgumentException("the new length " + length + " is larger than the fixed capacity " + capacity());
            }
            grow(length - capacity());
        }
        end = length;
        
        // If the pointer is beyond the new size, move it back to the new end...
        if (pos > end) {
            pos = end;
        }
    }
    
    /**
     * Grows the buffer by at least the number of specified bytes. A new buffer is
     * allocated, and the contents of the previous buffer are copied over.
     * 
     * @param need      the minimum number of extra bytes needed beyond the current capacity. 
     */
    private synchronized void grow(int need) {
        int size = capacity() + need;
        int below = Integer.highestOneBit(size);
        if (below != size) {
            size = below << 1;
        }
        byte[] newbuf = new byte[size];
        System.arraycopy(buf, 0, newbuf, 0, buf.length);
        buf = newbuf;
    }
    
    @Override
    public final synchronized void write(int b) throws IOException {
        if (pos + 1 > buf.length) {
            if (isGrowable) {
                grow(pos + 1 - buf.length);
            } else {
                throw new EOFException("buffer is full (size=" + length() + ")");
            }
        }
        buf[pos++] = (byte) b;
        if (pos > end) {
            end = pos;
        }
    }
    
    @Override
    public final synchronized void write(byte[] b, int from, int length) throws IOException {
        if (length <= 0) {
            return;
        }
        
        if (pos > buf.length || (isGrowable && pos + length > buf.length)) {
            // This may only happen in a growable buffer...
            grow(buf.length + length - pos);
        }
        
        int l = Math.min(length, buf.length - pos);
        
        System.arraycopy(b, from, buf, pos, l);
        pos += l;
        if (pos > end) {
            end = pos;
        }
        
        if (l < length) {
            throw new EOFException("Incomplete write of " + l + " of " + length + " bytes in buffer of size " + length());
        }
    }

    @Override
    public final synchronized int read() throws IOException {
        if (getRemaining() <= 0) {
            return -1;
        }
        return buf[pos++] & BYTE_MASK;
    }

    @Override
    public final synchronized int read(byte[] b, int from, int length) {
        if (length <= 0) {
            return 0;
        }
        
        int remaining = getRemaining();
        
        if (remaining <= 0) {
            return -1;
        }
        
        int n = Math.min(remaining, length);
        System.arraycopy(buf, pos, b, from, n);
        pos += n;
        
        return n;
    }
}