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}