/*
* 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 java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* {@link DataSource} backed by a {@link FileChannel} for {@link RandomAccessFile} access.
*/
public class FileChannelDataSource implements DataSource {
private static final int MAX_READ_CHUNK_SIZE = 1024 * 1024;
private final FileChannel mChannel;
private final long mOffset;
private final long mSize;
/**
* Constructs a new {@code FileChannelDataSource} based on the data contained in the
* whole file. Changes to the contents of the file, including the size of the file,
* will be visible in this data source.
*/
public FileChannelDataSource(FileChannel channel) {
mChannel = channel;
mOffset = 0;
mSize = -1;
}
/**
* Constructs a new {@code FileChannelDataSource} based on the data contained in the
* specified region of the provided file. Changes to the contents of the file will be visible in
* this data source.
*
* @throws IndexOutOfBoundsException if {@code offset} or {@code size} is negative.
*/
public FileChannelDataSource(FileChannel channel, long offset, long size) {
if (offset < 0) {
throw new IndexOutOfBoundsException("offset: " + size);
}
if (size < 0) {
throw new IndexOutOfBoundsException("size: " + size);
}
mChannel = channel;
mOffset = offset;
mSize = size;
}
@Override
public long size() {
if (mSize == -1) {
try {
return mChannel.size();
} catch (IOException e) {
return 0;
}
} else {
return mSize;
}
}
@Override
public FileChannelDataSource slice(long offset, long size) {
long sourceSize = size();
checkChunkValid(offset, size, sourceSize);
if ((offset == 0) && (size == sourceSize)) {
return this;
}
return new FileChannelDataSource(mChannel, mOffset + offset, size);
}
@Override
public void feed(long offset, long size, DataSink sink) throws IOException {
long sourceSize = size();
checkChunkValid(offset, size, sourceSize);
if (size == 0) {
return;
}
long chunkOffsetInFile = mOffset + offset;
long remaining = size;
ByteBuffer buf = ByteBuffer.allocateDirect((int) Math.min(remaining, MAX_READ_CHUNK_SIZE));
while (remaining > 0) {
int chunkSize = (int) Math.min(remaining, buf.capacity());
int chunkRemaining = chunkSize;
buf.limit(chunkSize);
synchronized (mChannel) {
mChannel.position(chunkOffsetInFile);
while (chunkRemaining > 0) {
int read = mChannel.read(buf);
if (read < 0) {
throw new IOException("Unexpected EOF encountered");
}
chunkRemaining -= read;
}
}
buf.flip();
sink.consume(buf);
buf.clear();
chunkOffsetInFile += chunkSize;
remaining -= chunkSize;
}
}
@Override
public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
long sourceSize = size();
checkChunkValid(offset, size, sourceSize);
if (size == 0) {
return;
}
if (size > dest.remaining()) {
throw new BufferOverflowException();
}
long offsetInFile = mOffset + offset;
int remaining = size;
int prevLimit = dest.limit();
try {
// FileChannel.read(ByteBuffer) reads up to dest.remaining(). Thus, we need to adjust
// the buffer's limit to avoid reading more than size bytes.
dest.limit(dest.position() + size);
while (remaining > 0) {
int chunkSize;
synchronized (mChannel) {
mChannel.position(offsetInFile);
chunkSize = mChannel.read(dest);
}
offsetInFile += chunkSize;
remaining -= chunkSize;
}
} finally {
dest.limit(prevLimit);
}
}
@Override
public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
if (size < 0) {
throw new IndexOutOfBoundsException("size: " + size);
}
ByteBuffer result = ByteBuffer.allocate(size);
copyTo(offset, size, result);
result.flip();
return result;
}
private static void checkChunkValid(long offset, long size, long sourceSize) {
if (offset < 0) {
throw new IndexOutOfBoundsException("offset: " + offset);
}
if (size < 0) {
throw new IndexOutOfBoundsException("size: " + size);
}
if (offset > sourceSize) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") > source size (" + sourceSize + ")");
}
long endOffset = offset + size;
if (endOffset < offset) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") + size (" + size + ") overflow");
}
if (endOffset > sourceSize) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") + size (" + size
+ ") > source size (" + sourceSize +")");
}
}
}