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}