/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.util;
import com.android.apksig.util.DataSink;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.ReadableDataSink;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
/**
* Growable byte array which can be appended to via {@link DataSink} interface and read from via
* {@link DataSource} interface.
*/
public class ByteArrayDataSink implements ReadableDataSink {
private static final int MAX_READ_CHUNK_SIZE = 65536;
private byte[] mArray;
private int mSize;
public ByteArrayDataSink() {
this(65536);
}
public ByteArrayDataSink(int initialCapacity) {
if (initialCapacity < 0) {
throw new IllegalArgumentException("initial capacity: " + initialCapacity);
}
mArray = new byte[initialCapacity];
}
@Override
public void consume(byte[] buf, int offset, int length) throws IOException {
if (offset < 0) {
// Must perform this check because System.arraycopy below doesn't perform it when
// length == 0
throw new IndexOutOfBoundsException("offset: " + offset);
}
if (offset > buf.length) {
// Must perform this check because System.arraycopy below doesn't perform it when
// length == 0
throw new IndexOutOfBoundsException(
"offset: " + offset + ", buf.length: " + buf.length);
}
if (length == 0) {
return;
}
ensureAvailable(length);
System.arraycopy(buf, offset, mArray, mSize, length);
mSize += length;
}
@Override
public void consume(ByteBuffer buf) throws IOException {
if (!buf.hasRemaining()) {
return;
}
if (buf.hasArray()) {
consume(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining());
buf.position(buf.limit());
return;
}
ensureAvailable(buf.remaining());
byte[] tmp = new byte[Math.min(buf.remaining(), MAX_READ_CHUNK_SIZE)];
while (buf.hasRemaining()) {
int chunkSize = Math.min(buf.remaining(), tmp.length);
buf.get(tmp, 0, chunkSize);
System.arraycopy(tmp, 0, mArray, mSize, chunkSize);
mSize += chunkSize;
}
}
private void ensureAvailable(int minAvailable) throws IOException {
if (minAvailable <= 0) {
return;
}
long minCapacity = ((long) mSize) + minAvailable;
if (minCapacity <= mArray.length) {
return;
}
if (minCapacity > Integer.MAX_VALUE) {
throw new IOException(
"Required capacity too large: " + minCapacity + ", max: " + Integer.MAX_VALUE);
}
int doubleCurrentSize = (int) Math.min(mArray.length * 2L, Integer.MAX_VALUE);
int newSize = (int) Math.max(minCapacity, doubleCurrentSize);
mArray = Arrays.copyOf(mArray, newSize);
}
@Override
public long size() {
return mSize;
}
@Override
public ByteBuffer getByteBuffer(long offset, int size) {
checkChunkValid(offset, size);
// checkChunkValid ensures that it's OK to cast offset to int.
return ByteBuffer.wrap(mArray, (int) offset, size).slice();
}
@Override
public void feed(long offset, long size, DataSink sink) throws IOException {
checkChunkValid(offset, size);
// checkChunkValid ensures that it's OK to cast offset and size to int.
sink.consume(mArray, (int) offset, (int) size);
}
@Override
public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
checkChunkValid(offset, size);
// checkChunkValid ensures that it's OK to cast offset to int.
dest.put(mArray, (int) offset, size);
}
private void checkChunkValid(long offset, long size) {
if (offset < 0) {
throw new IndexOutOfBoundsException("offset: " + offset);
}
if (size < 0) {
throw new IndexOutOfBoundsException("size: " + size);
}
if (offset > mSize) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") > source size (" + mSize + ")");
}
long endOffset = offset + size;
if (endOffset < offset) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") + size (" + size + ") overflow");
}
if (endOffset > mSize) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") + size (" + size + ") > source size (" + mSize + ")");
}
}
@Override
public DataSource slice(long offset, long size) {
checkChunkValid(offset, size);
// checkChunkValid ensures that it's OK to cast offset and size to int.
return new SliceDataSource((int) offset, (int) size);
}
/**
* Slice of the growable byte array. The slice's offset and size in the array are fixed.
*/
private class SliceDataSource implements DataSource {
private final int mSliceOffset;
private final int mSliceSize;
private SliceDataSource(int offset, int size) {
mSliceOffset = offset;
mSliceSize = size;
}
@Override
public long size() {
return mSliceSize;
}
@Override
public void feed(long offset, long size, DataSink sink) throws IOException {
checkChunkValid(offset, size);
// checkChunkValid combined with the way instances of this class are constructed ensures
// that mSliceOffset + offset does not overflow and that it's fine to cast size to int.
sink.consume(mArray, (int) (mSliceOffset + offset), (int) size);
}
@Override
public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
checkChunkValid(offset, size);
// checkChunkValid combined with the way instances of this class are constructed ensures
// that mSliceOffset + offset does not overflow.
return ByteBuffer.wrap(mArray, (int) (mSliceOffset + offset), size).slice();
}
@Override
public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
checkChunkValid(offset, size);
// checkChunkValid combined with the way instances of this class are constructed ensures
// that mSliceOffset + offset does not overflow.
dest.put(mArray, (int) (mSliceOffset + offset), size);
}
@Override
public DataSource slice(long offset, long size) {
checkChunkValid(offset, size);
// checkChunkValid combined with the way instances of this class are constructed ensures
// that mSliceOffset + offset does not overflow and that it's fine to cast size to int.
return new SliceDataSource((int) (mSliceOffset + offset), (int) size);
}
private void checkChunkValid(long offset, long size) {
if (offset < 0) {
throw new IndexOutOfBoundsException("offset: " + offset);
}
if (size < 0) {
throw new IndexOutOfBoundsException("size: " + size);
}
if (offset > mSliceSize) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") > source size (" + mSliceSize + ")");
}
long endOffset = offset + size;
if (endOffset < offset) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") + size (" + size + ") overflow");
}
if (endOffset > mSliceSize) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") + size (" + size + ") > source size (" + mSliceSize
+ ")");
}
}
}
}