    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *   http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 *
    017 */
    018
    019package org.apache.commons.compress.utils;
    020
    021import java.io.IOException;
    022import java.nio.ByteBuffer;
    023import java.nio.channels.ClosedChannelException;
    024import java.nio.channels.SeekableByteChannel;
    025import java.util.Arrays;
    026import java.util.concurrent.atomic.AtomicBoolean;
    027
    028/**
    029 * A {@link SeekableByteChannel} implementation that wraps a byte[].
    030 *
    031 * <p>When this channel is used for writing an internal buffer grows to accommodate incoming data. The natural size
    032 * limit is the value of {@link Integer#MAX_VALUE} and it is not possible to {@link #position(long) set the position} or
    033 * {@link #truncate truncate} to a value bigger than that.  Internal buffer can be accessed via {@link
    034 * SeekableInMemoryByteChannel#array()}.</p>
    035 *
    036 * @since 1.13
    037 * @NotThreadSafe
    038 */
    039public class SeekableInMemoryByteChannel implements SeekableByteChannel {
    040
    041    private static final int NAIVE_RESIZE_LIMIT = Integer.MAX_VALUE >> 1;
    042
    043    private byte[] data;
    044    private final AtomicBoolean closed = new AtomicBoolean();
    045    private int position, size;
    046
    047    /**
    048     * Constructor taking a byte array.
    049     *
    050     * <p>This constructor is intended to be used with pre-allocated buffer or when
    051     * reading from a given byte array.</p>
    052     *
    053     * @param data input data or pre-allocated array.
    054     */
    055    public SeekableInMemoryByteChannel(final byte[] data) {
    056        this.data = data;
    057        size = data.length;
    058    }
    059
    060    /**
    061     * Parameterless constructor - allocates internal buffer by itself.
    062     */
    063    public SeekableInMemoryByteChannel() {
    064        this(ByteUtils.EMPTY_BYTE_ARRAY);
    065    }
    066
    067    /**
    068     * Constructor taking a size of storage to be allocated.
    069     *
    070     * <p>Creates a channel and allocates internal storage of a given size.</p>
    071     *
    072     * @param size size of internal buffer to allocate, in bytes.
    073     */
    074    public SeekableInMemoryByteChannel(final int size) {
    075        this(new byte[size]);
    076    }
    077
    078    /**
    079     * Returns this channel's position.
    080     *
    081     * <p>This method violates the contract of {@link SeekableByteChannel#position()} as it will not throw any exception
    082     * when invoked on a closed channel. Instead it will return the position the channel had when close has been
    083     * called.</p>
    084     */
    085    @Override
    086    public long position() {
    087        return position;
    088    }
    089
    090    @Override
    091    public SeekableByteChannel position(final long newPosition) throws IOException {
    092        ensureOpen();
    093        if (newPosition < 0L || newPosition > Integer.MAX_VALUE) {
    094            throw new IOException("Position has to be in range 0.. " + Integer.MAX_VALUE);
    095        }
    096        position = (int) newPosition;
    097        return this;
    098    }
    099
    100    /**
    101     * Returns the current size of entity to which this channel is connected.
    102     *
    103     * <p>This method violates the contract of {@link SeekableByteChannel#size} as it will not throw any exception when
    104     * invoked on a closed channel. Instead it will return the size the channel had when close has been called.</p>
    105     */
    106    @Override
    107    public long size() {
    108        return size;
    109    }
    110
    111    /**
    112     * Truncates the entity, to which this channel is connected, to the given size.
    113     *
    114     * <p>This method violates the contract of {@link SeekableByteChannel#truncate} as it will not throw any exception when
    115     * invoked on a closed channel.</p>
    116     *
    117     * @throws IllegalArgumentException if size is negative or bigger than the maximum of a Java integer
    118     */
    119    @Override
    120    public SeekableByteChannel truncate(final long newSize) {
    121        if (newSize < 0L || newSize > Integer.MAX_VALUE) {
    122            throw new IllegalArgumentException("Size has to be in range 0.. " + Integer.MAX_VALUE);
    123        }
    124        if (size > newSize) {
    125            size = (int) newSize;
    126        }
    127        if (position > newSize) {
    128            position = (int) newSize;
    129        }
    130        return this;
    131    }
    132
    133    @Override
    134    public int read(final ByteBuffer buf) throws IOException {
    135        ensureOpen();
    136        int wanted = buf.remaining();
    137        final int possible = size - position;
    138        if (possible <= 0) {
    139            return -1;
    140        }
    141        if (wanted > possible) {
    142            wanted = possible;
    143        }
    144        buf.put(data, position, wanted);
    145        position += wanted;
    146        return wanted;
    147    }
    148
    149    @Override
    150    public void close() {
    151        closed.set(true);
    152    }
    153
    154    @Override
    155    public boolean isOpen() {
    156        return !closed.get();
    157    }
    158
    159    @Override
    160    public int write(final ByteBuffer b) throws IOException {
    161        ensureOpen();
    162        int wanted = b.remaining();
    163        final int possibleWithoutResize = size - position;
    164        if (wanted > possibleWithoutResize) {
    165            final int newSize = position + wanted;
    166            if (newSize < 0) { // overflow
    167                resize(Integer.MAX_VALUE);
    168                wanted = Integer.MAX_VALUE - position;
    169            } else {
    170                resize(newSize);
    171            }
    172        }
    173        b.get(data, position, wanted);
    174        position += wanted;
    175        if (size < position) {
    176            size = position;
    177        }
    178        return wanted;
    179    }
    180
    181    /**
    182     * Obtains the array backing this channel.
    183     *
    184     * <p>NOTE:
    185     * The returned buffer is not aligned with containing data, use
    186     * {@link #size()} to obtain the size of data stored in the buffer.</p>
    187     *
    188     * @return internal byte array.
    189     */
    190    public byte[] array() {
    191        return data;
    192    }
    193
    194    private void resize(final int newLength) {
    195        int len = data.length;
    196        if (len <= 0) {
    197            len = 1;
    198        }
    199        if (newLength < NAIVE_RESIZE_LIMIT) {
    200            while (len < newLength) {
    201                len <<= 1;
    202            }
    203        } else { // avoid overflow
    204            len = newLength;
    205        }
    206        data = Arrays.copyOf(data, len);
    207    }
    208
    209    private void ensureOpen() throws ClosedChannelException {
    210        if (!isOpen()) {
    211            throw new ClosedChannelException();
    212        }
    213    }
    214
    215}