// Copyright 2015 The Bazel Authors. All rights reserved. // // 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.google.devtools.build.android.ziputils; import com.google.common.base.Preconditions; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; /** * An API for reading big files through a direct byte buffer spanning a region of the file. * This object maintains an internal buffer, which may store all or some of the file content. * When a request for data is made ({@link #getBuffer(long, int) }, the implementation will * first determine if the requested data range is within the region specified at time of * construction. If it is, it checks to see if the request is within the capacity range of * the current internal buffer. If not, the buffer is reallocated, based at the requested offset. * Then the implementation checks to see if the requested data falls within the current fill limit * of the internal buffer. If not additional data is read from the file. Finally, a slice of * the internal buffer is returned, with the requested data. * * <p>This is optimized for forward scanning of files. Random access is supported, but will likely * be inefficient, especially if the entire file doesn't fit in the internal buffer. * * <p>Clients of this API should take care not to keep references to returned buffers indefinitely, * as this would prevent collection of buffers discarded by the {@code BufferedFile} object. */ public class BufferedFile { private int maxAlloc; private long offset; private long limit; private FileChannel channel; private ByteBuffer current; private long currOff; /** * Same as {@code BufferedFile(channel, 0, channel.size(), blockSize)}. * * @param channel file channel opened for reading. * @param blockSize maximum buffer allocation. * @throws NullPointerException if {@code channel} is {@code null}. * @throws IllegalArgumentException if {@code maxAlloc}, {@code off}, or {@code len} are negative * or if {@code off + len > channel.size()}. * @throws IOException */ public BufferedFile(FileChannel channel, int blockSize) throws IOException { this(channel, 0, channel.size(), blockSize); } /** * Allocates a buffered file. * * @param channel file channel opened for reading. * @param off the first byte that can be read through this object. * @param len the max number of bytes that can be read through this object. * @param blockSize default max buffer allocation size is {@code Math.min(blockSize, len)}. * @throws NullPointerException if {@code channel} is {@code null}. * @throws IllegalArgumentException if {@code blockSize}, {@code off}, or {@code len} are negative * or if {@code off + len > channel.size()}. * @throws IOException if thrown by the underlying file channel. */ public BufferedFile(FileChannel channel, long off, long len, int blockSize) throws IOException { Preconditions.checkNotNull(channel); Preconditions.checkArgument(blockSize >= 0); Preconditions.checkArgument(off >= 0); Preconditions.checkArgument(len >= 0); Preconditions.checkArgument(off + len <= channel.size()); this.maxAlloc = (int) Math.min(blockSize, len); this.offset = off; this.limit = off + len; this.channel = channel; this.current = null; currOff = -1; } /** * Returns the offset of the first byte beyond the readable region. * @return the file offset just beyond the readable region. */ public long limit() { return limit; } /** * Returns a byte buffer for reading {@code len} bytes from the {@code off} position * in the file. If the requested bytes are already loaded in the internal buffer, a slice is * returned, with position 0 and limit set to {@code len}. The slice may have a capacity greater * than its limit, if more bytes are already available in the internal buffer. If the requested * bytes are not available, but can fit in the current internal buffer, then more data is read, * before a slice is created as described above. If the requested data falls outside the range * that can be fitted into the current internal buffer, then a new internal buffer is allocated. * The prior internal buffer (if any), is no longer referenced by this object (but it may still * be referenced by the client, holding references to byte buffers returned from prior call to * this method). The new internal buffer will be based at {@code off} file position, and have a * capacity equal to the maximum of the {@code blockSize} of this buffer and {@code len}, except * that it will never exceed the the number of bytes from {@code off} to the end of the readable * region of the file (min-max rule). * * @param off * @param len * @return a slice of the internal byte buffer containing the requested data. Except, if the * client request data beyond the readable region of the file, the {@code len} value is reduced * to the maximum number of bytes available from the given {@code off}. * @throws IllegalArgumentException if {@code len} is less than 0, or {@code off} is outside the * readable region specified when constructing this object. * @throws IOException if thrown by the underlying file channel. */ public synchronized ByteBuffer getBuffer(long off, int len) throws IOException { Preconditions.checkArgument(off >= offset); Preconditions.checkArgument(len >= 0); Preconditions.checkArgument(off < limit || (off == limit && len == 0)); if (limit - off < len) { // never return data beyond limit len = (int) (limit - off); } Preconditions.checkState(off + len <= limit); if (current == null || off < currOff || off + len > currOff + current.capacity()) { allocate(off, len); Preconditions.checkState(current != null && off == currOff && off + len <= currOff + current.capacity()); } Preconditions.checkState(current != null && off >= currOff && off + len <= currOff + current.capacity()); if (off - currOff + len > current.limit()) { readMore((int) (off - currOff) + len); } Preconditions.checkState(current != null && off >= currOff && off + len <= currOff + current.limit()); current.position((int) (off - currOff)); return (ByteBuffer) current.slice().limit(len); } private void readMore(int newMin) throws IOException { channel.position(currOff + current.limit()); current.position(current.limit()); current.limit(current.capacity()); do { channel.read(current); } while(current.position() < newMin); current.limit(current.position()).position(0); } private void allocate(long off, int len) { current = ByteBuffer.allocateDirect(bufferSize(off, len)); current.limit(0); currOff = off; } private int bufferSize(long off, int len) { return (int) Math.min(Math.max(len, maxAlloc), limit - off); } }