aboutsummaryrefslogtreecommitdiff
path: root/jimfs/src/main/java/com/google/common/jimfs/RegularFile.java
diff options
context:
space:
mode:
Diffstat (limited to 'jimfs/src/main/java/com/google/common/jimfs/RegularFile.java')
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/RegularFile.java661
1 files changed, 661 insertions, 0 deletions
diff --git a/jimfs/src/main/java/com/google/common/jimfs/RegularFile.java b/jimfs/src/main/java/com/google/common/jimfs/RegularFile.java
new file mode 100644
index 0000000..b8bb688
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/RegularFile.java
@@ -0,0 +1,661 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.jimfs.Util.clear;
+import static com.google.common.jimfs.Util.nextPowerOf2;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.primitives.UnsignedBytes;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
+import java.util.Arrays;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/**
+ * A mutable, resizable store for bytes. Bytes are stored in fixed-sized byte arrays (blocks)
+ * allocated by a {@link HeapDisk}.
+ *
+ * @author Colin Decker
+ */
+final class RegularFile extends File {
+
+ private final ReadWriteLock lock = new ReentrantReadWriteLock();
+
+ private final HeapDisk disk;
+
+ /** Block list for the file. */
+ private byte[][] blocks;
+ /** Block count for the the file, which also acts as the head of the block list. */
+ private int blockCount;
+
+ private long size;
+
+ /** Creates a new regular file with the given ID and using the given disk. */
+ public static RegularFile create(int id, HeapDisk disk) {
+ return new RegularFile(id, disk, new byte[32][], 0, 0);
+ }
+
+ RegularFile(int id, HeapDisk disk, byte[][] blocks, int blockCount, long size) {
+ super(id);
+ this.disk = checkNotNull(disk);
+ this.blocks = checkNotNull(blocks);
+ this.blockCount = blockCount;
+
+ checkArgument(size >= 0);
+ this.size = size;
+ }
+
+ private int openCount = 0;
+ private boolean deleted = false;
+
+ /** Returns the read lock for this file. */
+ public Lock readLock() {
+ return lock.readLock();
+ }
+
+ /** Returns the write lock for this file. */
+ public Lock writeLock() {
+ return lock.writeLock();
+ }
+
+ // lower-level methods dealing with the blocks array
+
+ private void expandIfNecessary(int minBlockCount) {
+ if (minBlockCount > blocks.length) {
+ this.blocks = Arrays.copyOf(blocks, nextPowerOf2(minBlockCount));
+ }
+ }
+
+ /** Returns the number of blocks this file contains. */
+ int blockCount() {
+ return blockCount;
+ }
+
+ /** Copies the last {@code count} blocks from this file to the end of the given target file. */
+ void copyBlocksTo(RegularFile target, int count) {
+ int start = blockCount - count;
+ int targetEnd = target.blockCount + count;
+ target.expandIfNecessary(targetEnd);
+
+ System.arraycopy(this.blocks, start, target.blocks, target.blockCount, count);
+ target.blockCount = targetEnd;
+ }
+
+ /** Transfers the last {@code count} blocks from this file to the end of the given target file. */
+ void transferBlocksTo(RegularFile target, int count) {
+ copyBlocksTo(target, count);
+ truncateBlocks(blockCount - count);
+ }
+
+ /** Truncates the blocks of this file to the given block count. */
+ void truncateBlocks(int count) {
+ clear(blocks, count, blockCount - count);
+ blockCount = count;
+ }
+
+ /** Adds the given block to the end of this file. */
+ void addBlock(byte[] block) {
+ expandIfNecessary(blockCount + 1);
+ blocks[blockCount++] = block;
+ }
+
+ /** Gets the block at the given index in this file. */
+ @VisibleForTesting
+ byte[] getBlock(int index) {
+ return blocks[index];
+ }
+
+ // end of lower-level methods dealing with the blocks array
+
+ /**
+ * Gets the current size of this file in bytes. Does not do locking, so should only be called when
+ * holding a lock.
+ */
+ public long sizeWithoutLocking() {
+ return size;
+ }
+
+ // need to lock in these methods since they're defined by an interface
+
+ @Override
+ public long size() {
+ readLock().lock();
+ try {
+ return size;
+ } finally {
+ readLock().unlock();
+ }
+ }
+
+ @Override
+ RegularFile copyWithoutContent(int id) {
+ byte[][] copyBlocks = new byte[Math.max(blockCount * 2, 32)][];
+ return new RegularFile(id, disk, copyBlocks, 0, size);
+ }
+
+ @Override
+ void copyContentTo(File file) throws IOException {
+ RegularFile copy = (RegularFile) file;
+ disk.allocate(copy, blockCount);
+
+ for (int i = 0; i < blockCount; i++) {
+ byte[] block = blocks[i];
+ byte[] copyBlock = copy.blocks[i];
+ System.arraycopy(block, 0, copyBlock, 0, block.length);
+ }
+ }
+
+ @Override
+ ReadWriteLock contentLock() {
+ return lock;
+ }
+
+ // opened/closed/delete don't use the read/write lock... they only need to ensure that they are
+ // synchronized among themselves
+
+ @Override
+ public synchronized void opened() {
+ openCount++;
+ }
+
+ @Override
+ public synchronized void closed() {
+ if (--openCount == 0 && deleted) {
+ deleteContents();
+ }
+ }
+
+ /**
+ * Marks this file as deleted. If there are no streams or channels open to the file, its contents
+ * are deleted if necessary.
+ */
+ @Override
+ public synchronized void deleted() {
+ if (links() == 0) {
+ deleted = true;
+ if (openCount == 0) {
+ deleteContents();
+ }
+ }
+ }
+
+ /**
+ * Deletes the contents of this file. Called when this file has been deleted and all open streams
+ * and channels to it have been closed.
+ */
+ private void deleteContents() {
+ disk.free(this);
+ size = 0;
+ }
+
+ /**
+ * Truncates this file to the given {@code size}. If the given size is less than the current size
+ * of this file, the size of the file is reduced to the given size and any bytes beyond that size
+ * are lost. If the given size is greater than the current size of the file, this method does
+ * nothing. Returns {@code true} if this file was modified by the call (its size changed) and
+ * {@code false} otherwise.
+ */
+ public boolean truncate(long size) {
+ if (size >= this.size) {
+ return false;
+ }
+
+ long lastPosition = size - 1;
+ this.size = size;
+
+ int newBlockCount = blockIndex(lastPosition) + 1;
+ int blocksToRemove = blockCount - newBlockCount;
+ if (blocksToRemove > 0) {
+ disk.free(this, blocksToRemove);
+ }
+
+ return true;
+ }
+
+ /** Prepares for a write of len bytes starting at position pos. */
+ private void prepareForWrite(long pos, long len) throws IOException {
+ long end = pos + len;
+
+ // allocate any additional blocks needed
+ int lastBlockIndex = blockCount - 1;
+ int endBlockIndex = blockIndex(end - 1);
+
+ if (endBlockIndex > lastBlockIndex) {
+ int additionalBlocksNeeded = endBlockIndex - lastBlockIndex;
+ disk.allocate(this, additionalBlocksNeeded);
+ }
+
+ // zero bytes between current size and pos
+ if (pos > size) {
+ long remaining = pos - size;
+
+ int blockIndex = blockIndex(size);
+ byte[] block = blocks[blockIndex];
+ int off = offsetInBlock(size);
+
+ remaining -= zero(block, off, length(off, remaining));
+
+ while (remaining > 0) {
+ block = blocks[++blockIndex];
+
+ remaining -= zero(block, 0, length(remaining));
+ }
+
+ size = pos;
+ }
+ }
+
+ /**
+ * Writes the given byte to this file at position {@code pos}. {@code pos} may be greater than the
+ * current size of this file, in which case this file is resized and all bytes between the current
+ * size and {@code pos} are set to 0. Returns the number of bytes written.
+ *
+ * @throws IOException if the file needs more blocks but the disk is full
+ */
+ public int write(long pos, byte b) throws IOException {
+ prepareForWrite(pos, 1);
+
+ byte[] block = blocks[blockIndex(pos)];
+ int off = offsetInBlock(pos);
+ block[off] = b;
+
+ if (pos >= size) {
+ size = pos + 1;
+ }
+
+ return 1;
+ }
+
+ /**
+ * Writes {@code len} bytes starting at offset {@code off} in the given byte array to this file
+ * starting at position {@code pos}. {@code pos} may be greater than the current size of this
+ * file, in which case this file is resized and all bytes between the current size and {@code pos}
+ * are set to 0. Returns the number of bytes written.
+ *
+ * @throws IOException if the file needs more blocks but the disk is full
+ */
+ public int write(long pos, byte[] b, int off, int len) throws IOException {
+ prepareForWrite(pos, len);
+
+ if (len == 0) {
+ return 0;
+ }
+
+ int remaining = len;
+
+ int blockIndex = blockIndex(pos);
+ byte[] block = blocks[blockIndex];
+ int offInBlock = offsetInBlock(pos);
+
+ int written = put(block, offInBlock, b, off, length(offInBlock, remaining));
+ remaining -= written;
+ off += written;
+
+ while (remaining > 0) {
+ block = blocks[++blockIndex];
+
+ written = put(block, 0, b, off, length(remaining));
+ remaining -= written;
+ off += written;
+ }
+
+ long endPos = pos + len;
+ if (endPos > size) {
+ size = endPos;
+ }
+
+ return len;
+ }
+
+ /**
+ * Writes all available bytes from buffer {@code buf} to this file starting at position {@code
+ * pos}. {@code pos} may be greater than the current size of this file, in which case this file is
+ * resized and all bytes between the current size and {@code pos} are set to 0. Returns the number
+ * of bytes written.
+ *
+ * @throws IOException if the file needs more blocks but the disk is full
+ */
+ public int write(long pos, ByteBuffer buf) throws IOException {
+ int len = buf.remaining();
+
+ prepareForWrite(pos, len);
+
+ if (len == 0) {
+ return 0;
+ }
+
+ int blockIndex = blockIndex(pos);
+ byte[] block = blocks[blockIndex];
+ int off = offsetInBlock(pos);
+
+ put(block, off, buf);
+
+ while (buf.hasRemaining()) {
+ block = blocks[++blockIndex];
+
+ put(block, 0, buf);
+ }
+
+ long endPos = pos + len;
+ if (endPos > size) {
+ size = endPos;
+ }
+
+ return len;
+ }
+
+ /**
+ * Writes all available bytes from each buffer in {@code bufs}, in order, to this file starting at
+ * position {@code pos}. {@code pos} may be greater than the current size of this file, in which
+ * case this file is resized and all bytes between the current size and {@code pos} are set to 0.
+ * Returns the number of bytes written.
+ *
+ * @throws IOException if the file needs more blocks but the disk is full
+ */
+ public long write(long pos, Iterable<ByteBuffer> bufs) throws IOException {
+ long start = pos;
+ for (ByteBuffer buf : bufs) {
+ pos += write(pos, buf);
+ }
+ return pos - start;
+ }
+
+ /**
+ * Transfers up to {@code count} bytes from the given channel to this file starting at position
+ * {@code pos}. Returns the number of bytes transferred. If {@code pos} is greater than the
+ * current size of this file, the file is truncated up to size {@code pos} before writing.
+ *
+ * @throws IOException if the file needs more blocks but the disk is full or if reading from src
+ * throws an exception
+ */
+ public long transferFrom(ReadableByteChannel src, long pos, long count) throws IOException {
+ prepareForWrite(pos, 0); // don't assume the full count bytes will be written
+
+ if (count == 0) {
+ return 0;
+ }
+
+ long remaining = count;
+
+ int blockIndex = blockIndex(pos);
+ byte[] block = blockForWrite(blockIndex);
+ int off = offsetInBlock(pos);
+
+ ByteBuffer buf = ByteBuffer.wrap(block, off, length(off, remaining));
+
+ long currentPos = pos;
+ int read = 0;
+ while (buf.hasRemaining()) {
+ read = src.read(buf);
+ if (read == -1) {
+ break;
+ }
+
+ currentPos += read;
+ remaining -= read;
+ }
+
+ // update size before trying to get next block in case the disk is out of space
+ if (currentPos > size) {
+ size = currentPos;
+ }
+
+ if (read != -1) {
+ outer:
+ while (remaining > 0) {
+ block = blockForWrite(++blockIndex);
+
+ buf = ByteBuffer.wrap(block, 0, length(remaining));
+ while (buf.hasRemaining()) {
+ read = src.read(buf);
+ if (read == -1) {
+ break outer;
+ }
+
+ currentPos += read;
+ remaining -= read;
+ }
+
+ if (currentPos > size) {
+ size = currentPos;
+ }
+ }
+ }
+
+ if (currentPos > size) {
+ size = currentPos;
+ }
+
+ return currentPos - pos;
+ }
+
+ /**
+ * Reads the byte at position {@code pos} in this file as an unsigned integer in the range 0-255.
+ * If {@code pos} is greater than or equal to the size of this file, returns -1 instead.
+ */
+ public int read(long pos) {
+ if (pos >= size) {
+ return -1;
+ }
+
+ byte[] block = blocks[blockIndex(pos)];
+ int off = offsetInBlock(pos);
+ return UnsignedBytes.toInt(block[off]);
+ }
+
+ /**
+ * Reads up to {@code len} bytes starting at position {@code pos} in this file to the given byte
+ * array starting at offset {@code off}. Returns the number of bytes actually read or -1 if {@code
+ * pos} is greater than or equal to the size of this file.
+ */
+ public int read(long pos, byte[] b, int off, int len) {
+ // since max is len (an int), result is guaranteed to be an int
+ int bytesToRead = (int) bytesToRead(pos, len);
+
+ if (bytesToRead > 0) {
+ int remaining = bytesToRead;
+
+ int blockIndex = blockIndex(pos);
+ byte[] block = blocks[blockIndex];
+ int offsetInBlock = offsetInBlock(pos);
+
+ int read = get(block, offsetInBlock, b, off, length(offsetInBlock, remaining));
+ remaining -= read;
+ off += read;
+
+ while (remaining > 0) {
+ int index = ++blockIndex;
+ block = blocks[index];
+
+ read = get(block, 0, b, off, length(remaining));
+ remaining -= read;
+ off += read;
+ }
+ }
+
+ return bytesToRead;
+ }
+
+ /**
+ * Reads up to {@code buf.remaining()} bytes starting at position {@code pos} in this file to the
+ * given buffer. Returns the number of bytes read or -1 if {@code pos} is greater than or equal to
+ * the size of this file.
+ */
+ public int read(long pos, ByteBuffer buf) {
+ // since max is buf.remaining() (an int), result is guaranteed to be an int
+ int bytesToRead = (int) bytesToRead(pos, buf.remaining());
+
+ if (bytesToRead > 0) {
+ int remaining = bytesToRead;
+
+ int blockIndex = blockIndex(pos);
+ byte[] block = blocks[blockIndex];
+ int off = offsetInBlock(pos);
+
+ remaining -= get(block, off, buf, length(off, remaining));
+
+ while (remaining > 0) {
+ int index = ++blockIndex;
+ block = blocks[index];
+ remaining -= get(block, 0, buf, length(remaining));
+ }
+ }
+
+ return bytesToRead;
+ }
+
+ /**
+ * Reads up to the total {@code remaining()} number of bytes in each of {@code bufs} starting at
+ * position {@code pos} in this file to the given buffers, in order. Returns the number of bytes
+ * read or -1 if {@code pos} is greater than or equal to the size of this file.
+ */
+ public long read(long pos, Iterable<ByteBuffer> bufs) {
+ if (pos >= size()) {
+ return -1;
+ }
+
+ long start = pos;
+ for (ByteBuffer buf : bufs) {
+ int read = read(pos, buf);
+ if (read == -1) {
+ break;
+ } else {
+ pos += read;
+ }
+ }
+
+ return pos - start;
+ }
+
+ /**
+ * Transfers up to {@code count} bytes to the given channel starting at position {@code pos} in
+ * this file. Returns the number of bytes transferred, possibly 0. Note that unlike all other read
+ * methods in this class, this method does not return -1 if {@code pos} is greater than or equal
+ * to the current size. This for consistency with {@link FileChannel#transferTo}, which this
+ * method is primarily intended as an implementation of.
+ */
+ public long transferTo(long pos, long count, WritableByteChannel dest) throws IOException {
+ long bytesToRead = bytesToRead(pos, count);
+
+ if (bytesToRead > 0) {
+ long remaining = bytesToRead;
+
+ int blockIndex = blockIndex(pos);
+ byte[] block = blocks[blockIndex];
+ int off = offsetInBlock(pos);
+
+ ByteBuffer buf = ByteBuffer.wrap(block, off, length(off, remaining));
+ while (buf.hasRemaining()) {
+ remaining -= dest.write(buf);
+ }
+ buf.clear();
+
+ while (remaining > 0) {
+ int index = ++blockIndex;
+ block = blocks[index];
+
+ buf = ByteBuffer.wrap(block, 0, length(remaining));
+ while (buf.hasRemaining()) {
+ remaining -= dest.write(buf);
+ }
+ buf.clear();
+ }
+ }
+
+ return Math.max(bytesToRead, 0); // don't return -1 for this method
+ }
+
+ /** Gets the block at the given index, expanding to create the block if necessary. */
+ private byte[] blockForWrite(int index) throws IOException {
+ if (index >= blockCount) {
+ int additionalBlocksNeeded = index - blockCount + 1;
+ disk.allocate(this, additionalBlocksNeeded);
+ }
+
+ return blocks[index];
+ }
+
+ private int blockIndex(long position) {
+ return (int) (position / disk.blockSize());
+ }
+
+ private int offsetInBlock(long position) {
+ return (int) (position % disk.blockSize());
+ }
+
+ private int length(long max) {
+ return (int) Math.min(disk.blockSize(), max);
+ }
+
+ private int length(int off, long max) {
+ return (int) Math.min(disk.blockSize() - off, max);
+ }
+
+ /**
+ * Returns the number of bytes that can be read starting at position {@code pos} (up to a maximum
+ * of {@code max}) or -1 if {@code pos} is greater than or equal to the current size.
+ */
+ private long bytesToRead(long pos, long max) {
+ long available = size - pos;
+ if (available <= 0) {
+ return -1;
+ }
+ return Math.min(available, max);
+ }
+
+ /** Zeroes len bytes in the given block starting at the given offset. Returns len. */
+ private static int zero(byte[] block, int offset, int len) {
+ Util.zero(block, offset, len);
+ return len;
+ }
+
+ /** Puts the given slice of the given array at the given offset in the given block. */
+ private static int put(byte[] block, int offset, byte[] b, int off, int len) {
+ System.arraycopy(b, off, block, offset, len);
+ return len;
+ }
+
+ /** Puts the contents of the given byte buffer at the given offset in the given block. */
+ private static int put(byte[] block, int offset, ByteBuffer buf) {
+ int len = Math.min(block.length - offset, buf.remaining());
+ buf.get(block, offset, len);
+ return len;
+ }
+
+ /**
+ * Reads len bytes starting at the given offset in the given block into the given slice of the
+ * given byte array.
+ */
+ private static int get(byte[] block, int offset, byte[] b, int off, int len) {
+ System.arraycopy(block, offset, b, off, len);
+ return len;
+ }
+
+ /** Reads len bytes starting at the given offset in the given block into the given byte buffer. */
+ private static int get(byte[] block, int offset, ByteBuffer buf, int len) {
+ buf.put(block, offset, len);
+ return len;
+ }
+}