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     * Parameterless constructor - allocates internal buffer by itself.
049     */
050    public SeekableInMemoryByteChannel() {
051        this(ByteUtils.EMPTY_BYTE_ARRAY);
052    }
053
054    /**
055     * Constructor taking a byte array.
056     *
057     * <p>This constructor is intended to be used with pre-allocated buffer or when
058     * reading from a given byte array.</p>
059     *
060     * @param data input data or pre-allocated array.
061     */
062    public SeekableInMemoryByteChannel(final byte[] data) {
063        this.data = data;
064        size = data.length;
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     * Obtains the array backing this channel.
080     *
081     * <p>NOTE:
082     * The returned buffer is not aligned with containing data, use
083     * {@link #size()} to obtain the size of data stored in the buffer.</p>
084     *
085     * @return internal byte array.
086     */
087    public byte[] array() {
088        return data;
089    }
090
091    @Override
092    public void close() {
093        closed.set(true);
094    }
095
096    private void ensureOpen() throws ClosedChannelException {
097        if (!isOpen()) {
098            throw new ClosedChannelException();
099        }
100    }
101
102    @Override
103    public boolean isOpen() {
104        return !closed.get();
105    }
106
107    /**
108     * Returns this channel's position.
109     *
110     * <p>This method violates the contract of {@link SeekableByteChannel#position()} as it will not throw any exception
111     * when invoked on a closed channel. Instead it will return the position the channel had when close has been
112     * called.</p>
113     */
114    @Override
115    public long position() {
116        return position;
117    }
118
119    @Override
120    public SeekableByteChannel position(final long newPosition) throws IOException {
121        ensureOpen();
122        if (newPosition < 0L || newPosition > Integer.MAX_VALUE) {
123            throw new IOException("Position has to be in range 0.. " + Integer.MAX_VALUE);
124        }
125        position = (int) newPosition;
126        return this;
127    }
128
129    @Override
130    public int read(final ByteBuffer buf) throws IOException {
131        ensureOpen();
132        int wanted = buf.remaining();
133        final int possible = size - position;
134        if (possible <= 0) {
135            return -1;
136        }
137        if (wanted > possible) {
138            wanted = possible;
139        }
140        buf.put(data, position, wanted);
141        position += wanted;
142        return wanted;
143    }
144
145    private void resize(final int newLength) {
146        int len = data.length;
147        if (len <= 0) {
148            len = 1;
149        }
150        if (newLength < NAIVE_RESIZE_LIMIT) {
151            while (len < newLength) {
152                len <<= 1;
153            }
154        } else { // avoid overflow
155            len = newLength;
156        }
157        data = Arrays.copyOf(data, len);
158    }
159
160    /**
161     * Returns the current size of entity to which this channel is connected.
162     *
163     * <p>This method violates the contract of {@link SeekableByteChannel#size} as it will not throw any exception when
164     * invoked on a closed channel. Instead it will return the size the channel had when close has been called.</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     *
174     * <p>This method violates the contract of {@link SeekableByteChannel#truncate} as it will not throw any exception when
175     * invoked on a closed channel.</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}