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
018package org.apache.commons.compress.utils;
019
020import java.io.IOException;
021import java.nio.ByteBuffer;
022import java.nio.channels.ClosedChannelException;
023import java.nio.channels.SeekableByteChannel;
024import java.util.Arrays;
025import java.util.concurrent.atomic.AtomicBoolean;
026
027/**
028 * A {@link SeekableByteChannel} implementation that wraps a byte[].
029 * <p>
030 * 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}
031 * 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
032 * via {@link SeekableInMemoryByteChannel#array()}.
033 * </p>
034 *
035 * @since 1.13
036 * @NotThreadSafe
037 */
038public class SeekableInMemoryByteChannel implements SeekableByteChannel {
039
040    private static final int NAIVE_RESIZE_LIMIT = Integer.MAX_VALUE >> 1;
041
042    private byte[] data;
043    private final AtomicBoolean closed = new AtomicBoolean();
044    private int position, size;
045
046    /**
047     * Constructs a new instance using a default empty buffer.
048     */
049    public SeekableInMemoryByteChannel() {
050        this(ByteUtils.EMPTY_BYTE_ARRAY);
051    }
052
053    /**
054     * Constructs a new instance from a byte array.
055     * <p>
056     * This constructor is intended to be used with pre-allocated buffer or when reading from a given byte array.
057     * </p>
058     *
059     * @param data input data or pre-allocated array.
060     */
061    public SeekableInMemoryByteChannel(final byte[] data) {
062        this.data = data;
063        this.size = data.length;
064    }
065
066    /**
067     * Constructs a new instance from a size of storage to be allocated.
068     * <p>
069     * Creates a channel and allocates internal storage of a given size.
070     * </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     * Obtains the array backing this channel.
080     * <p>
081     * NOTE: The returned buffer is not aligned with containing data, use {@link #size()} to obtain the size of data stored in the buffer.
082     * </p>
083     *
084     * @return internal byte array.
085     */
086    public byte[] array() {
087        return data;
088    }
089
090    @Override
091    public void close() {
092        closed.set(true);
093    }
094
095    private void ensureOpen() throws ClosedChannelException {
096        if (!isOpen()) {
097            throw new ClosedChannelException();
098        }
099    }
100
101    @Override
102    public boolean isOpen() {
103        return !closed.get();
104    }
105
106    /**
107     * Returns this channel's position.
108     * <p>
109     * This method violates the contract of {@link SeekableByteChannel#position()} as it will not throw any exception when invoked on a closed channel. Instead
110     * it will return the position the channel had when close has been called.
111     * </p>
112     */
113    @Override
114    public long position() {
115        return position;
116    }
117
118    @Override
119    public SeekableByteChannel position(final long newPosition) throws IOException {
120        ensureOpen();
121        if (newPosition < 0L || newPosition > Integer.MAX_VALUE) {
122            throw new IOException("Position has to be in range 0.. " + Integer.MAX_VALUE);
123        }
124        position = (int) newPosition;
125        return this;
126    }
127
128    @Override
129    public int read(final ByteBuffer buf) throws IOException {
130        ensureOpen();
131        int wanted = buf.remaining();
132        final int possible = size - position;
133        if (possible <= 0) {
134            return -1;
135        }
136        if (wanted > possible) {
137            wanted = possible;
138        }
139        buf.put(data, position, wanted);
140        position += wanted;
141        return wanted;
142    }
143
144    private void resize(final int newLength) {
145        int len = data.length;
146        if (len <= 0) {
147            len = 1;
148        }
149        if (newLength < NAIVE_RESIZE_LIMIT) {
150            while (len < newLength) {
151                len <<= 1;
152            }
153        } else { // avoid overflow
154            len = newLength;
155        }
156        data = Arrays.copyOf(data, len);
157    }
158
159    /**
160     * Returns the current size of entity to which this channel is connected.
161     * <p>
162     * This method violates the contract of {@link SeekableByteChannel#size} as it will not throw any exception when invoked on a closed channel. Instead it
163     * will return the size the channel had when close has been called.
164     * </p>
165     */
166    @Override
167    public long size() {
168        return size;
169    }
170
171    /**
172     * Truncates the entity, to which this channel is connected, to the given size.
173     * <p>
174     * This method violates the contract of {@link SeekableByteChannel#truncate} as it will not throw any exception when invoked on a closed channel.
175     * </p>
176     *
177     * @throws IllegalArgumentException if size is negative or bigger than the maximum of a Java integer
178     */
179    @Override
180    public SeekableByteChannel truncate(final long newSize) {
181        if (newSize < 0L || newSize > Integer.MAX_VALUE) {
182            throw new IllegalArgumentException("Size has to be in range 0.. " + Integer.MAX_VALUE);
183        }
184        if (size > newSize) {
185            size = (int) newSize;
186        }
187        if (position > newSize) {
188            position = (int) newSize;
189        }
190        return this;
191    }
192
193    @Override
194    public int write(final ByteBuffer b) throws IOException {
195        ensureOpen();
196        int wanted = b.remaining();
197        final int possibleWithoutResize = size - position;
198        if (wanted > possibleWithoutResize) {
199            final int newSize = position + wanted;
200            if (newSize < 0) { // overflow
201                resize(Integer.MAX_VALUE);
202                wanted = Integer.MAX_VALUE - position;
203            } else {
204                resize(newSize);
205            }
206        }
207        b.get(data, position, wanted);
208        position += wanted;
209        if (size < position) {
210            size = position;
211        }
212        return wanted;
213    }
214
215}