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