package com.github.marschall.memoryfilesystem;
import static com.github.marschall.memoryfilesystem.AutoReleaseLock.autoRelease;
import static java.lang.Math.max;
import static java.lang.Math.min;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
final class MemoryInode {
/*
* It turned out to be easier to implement the contents in this class
* rather than a separate class since it needs access to a, c, m times
* and update the open count.
*
* #transferTo and #transferFrom are candidates for deadlocks since they
* acquire two locks without ordering. However one is a read lock
* and the other is a write lock. So I "think" we're fine for now.
*/
/**
* The object header size of an array. Two words (flags & class oop)
* plus array size (2 *64 bit + 32 bit on 64 bit, 2 *32 bit + 32 bit on 32 bit).
*/
private static final int ARRAY_HEADER = 8 + 8 + 4;
static final int BLOCK_SIZE = 4096 - ARRAY_HEADER; //make sure it fits into a 4k memory region
static final int NUMBER_OF_BLOCKS = BLOCK_SIZE;
// lazily allocated, most files probably won't need this
private LockSet lockSet;
/**
* To store the contents efficiently we store the first {@value #BLOCK_SIZE}
* bytes in a {@value #BLOCK_SIZE} direct {@code byte[]}. The next
* {@value #NUMBER_OF_BLOCKS} * {@value #BLOCK_SIZE} bytes go into a indirect
* {@code byte[][]} that is lazily allocated.
*/
private byte[] directBlock;
private byte[][] indirectBlocks;
// TODO
// byte[][][] doubleIndirectBlocks
// TODO max link count
private long size;
private int indirectBlocksAllocated;
private final ReadWriteLock lock;
MemoryInode(int initialBlocks) {
this.lock = new ReentrantReadWriteLock();
if (initialBlocks == 0) {
// TODO could be made smaller
this.directBlock = new byte[BLOCK_SIZE];
} else {
this.directBlock = new byte[BLOCK_SIZE];
}
if (initialBlocks > 1) {
this.indirectBlocks = new byte[BLOCK_SIZE][];
for (int i = 0; i < initialBlocks - 1; ++i) {
this.indirectBlocks[i] = new byte[BLOCK_SIZE];
}
this.indirectBlocksAllocated = initialBlocks - 1;
}
this.size = 0L;
}
MemoryInode(MemoryInode other) {
this.lock = new ReentrantReadWriteLock();
if (other.directBlock != null) {
this.directBlock = other.directBlock.clone();
}
if (other.indirectBlocks != null) {
this.indirectBlocks = other.indirectBlocks.clone();
for (int i = 0; i < other.indirectBlocksAllocated; ++i) {
this.indirectBlocks[i] = other.indirectBlocks[i].clone();
}
}
this.indirectBlocksAllocated = other.indirectBlocksAllocated;
this.size = other.size;
}
AutoRelease readLock() {
return autoRelease(this.lock.readLock());
}
AutoRelease writeLock() {
return autoRelease(this.lock.writeLock());
}
long size() {
try (AutoRelease lock = this.readLock()) {
return this.size;
}
}
long read(ByteBuffer dst, long position, long maximum) throws IOException {
try (AutoRelease lock = this.readLock()) {
if (position >= this.size) {
return -1L;
}
long remaining = dst.remaining();
long toRead = min(min(this.size - position, remaining), maximum);
int currentBlock = (int) (position / BLOCK_SIZE);
int startIndexInBlock = (int) (position - (currentBlock * (long) BLOCK_SIZE));
long read = 0L;
while (read < toRead) {
int lengthInBlock = (int) min((long) BLOCK_SIZE - startIndexInBlock, toRead - read);
byte[] block = this.getBlock(currentBlock);
dst.put(block, startIndexInBlock, lengthInBlock);
read += lengthInBlock;
startIndexInBlock = 0;
currentBlock += 1;
}
return read;
}
}
int readShort(ByteBuffer dst, long position) throws IOException {
return (int) this.read(dst, position, Integer.MAX_VALUE);
}
int read(byte[] dst, long position, int off, int len) throws IOException {
try (AutoRelease lock = this.readLock()) {
if (position >= this.size) {
return -1;
}
int toRead = (int) min(min(this.size - position, len), Integer.MAX_VALUE);
int currentBlock = (int) (position / BLOCK_SIZE);
int startIndexInBlock = (int) (position - (currentBlock * (long) BLOCK_SIZE));
int read = 0;
while (read < toRead) {
int lengthInBlock = (int) min((long) BLOCK_SIZE - startIndexInBlock, (long) toRead - (long) read);
byte[] block = this.getBlock(currentBlock);
System.arraycopy(block, startIndexInBlock, dst, off + read, lengthInBlock);
read += lengthInBlock;
startIndexInBlock = 0;
currentBlock += 1;
}
return read;
}
}
long transferFrom(ReadableByteChannel src, long position, long count) throws IOException {
try (AutoRelease lock = this.writeLock()) {
this.ensureCapacity(position + count);
long transferred = 0L;
long toTransfer = count;
int currentBlock = (int) (position / BLOCK_SIZE);
int startIndexInBlock = (int) (position - (currentBlock * (long) BLOCK_SIZE));
while (transferred < toTransfer) {
int lengthInBlock = (int) min((long) BLOCK_SIZE - startIndexInBlock, toTransfer - transferred);
byte[] block = this.getBlock(currentBlock);
// We can either allocate a new ByteBuffer for every iteration or keep
// the buffer and copy the contents into it.
// Since ByteBuffer objects are quite small and don't copy the contents
// of the backing array allocating a ByteBuffer is probably cheaper.
ByteBuffer buffer = ByteBuffer.wrap(block, startIndexInBlock, lengthInBlock);
readFully(buffer, src, lengthInBlock);
transferred += lengthInBlock;
startIndexInBlock = 0;
currentBlock += 1;
}
// REVIEW, possibility to fill with random data
this.size = max(this.size, position + transferred);
return transferred;
}
}
long transferTo(WritableByteChannel target, long position, long count) throws IOException {
try (AutoRelease lock = this.readLock()) {
long transferred = 0L;
long toTransfer = min(count, this.size - position);
int currentBlock = (int) (position / BLOCK_SIZE);
int startIndexInBlock = (int) (position - (currentBlock * (long) BLOCK_SIZE));
while (transferred < toTransfer) {
int lengthInBlock = (int) min((long) BLOCK_SIZE - startIndexInBlock, toTransfer - transferred);
byte[] block = this.getBlock(currentBlock);
// We can either allocate a new ByteBuffer for every iteration or keep
// the buffer and copy the contents into it.
// Since ByteBuffer objects are quite small and don't copy the contents
// of the backing array allocating a ByteBuffer is probably cheaper.
ByteBuffer buffer = ByteBuffer.wrap(block, startIndexInBlock, lengthInBlock);
writeFully(buffer, target, lengthInBlock);
transferred += lengthInBlock;
startIndexInBlock = 0;
currentBlock += 1;
}
return transferred;
}
}
long write(ByteBuffer src, long position, long maximum) {
try (AutoRelease lock = this.writeLock()) {
long remaining = src.remaining();
this.ensureCapacity(position + remaining);
long toWrite = min(remaining, maximum);
int currentBlock = (int) (position / BLOCK_SIZE);
int startIndexInBlock = (int) (position - (currentBlock * (long) BLOCK_SIZE));
long written = 0L;
while (written < toWrite) {
int lengthInBlock = (int) min((long) BLOCK_SIZE - startIndexInBlock, toWrite - written);
byte[] block = this.getBlock(currentBlock);
src.get(block, startIndexInBlock, lengthInBlock);
written += lengthInBlock;
startIndexInBlock = 0;
currentBlock += 1;
}
// REVIEW, possibility to fill with random data
this.size = max(this.size, position + written);
return written;
}
}
int writeShort(ByteBuffer src, long position) {
return (int) this.write(src, position, Integer.MAX_VALUE);
}
int write(byte[] src, long position, int off, int len) {
try (AutoRelease lock = this.writeLock()) {
this.ensureCapacity(position + len);
int toWrite = min(len, Integer.MAX_VALUE);
int currentBlock = (int) (position / BLOCK_SIZE);
int startIndexInBlock = (int) (position - (currentBlock * (long) BLOCK_SIZE));
int written = 0;
while (written < toWrite) {
int lengthInBlock = (int) min((long) BLOCK_SIZE - startIndexInBlock, (long) toWrite - (long) written);
byte[] block = this.getBlock(currentBlock);
System.arraycopy(src, off + written, block, startIndexInBlock, lengthInBlock);
written += lengthInBlock;
startIndexInBlock = 0;
currentBlock += 1;
}
// REVIEW, possibility to fill with random data
this.size = max(this.size, position + written);
return written;
}
}
long writeAtEnd(ByteBuffer src, long maximum) {
try (AutoRelease lock = this.writeLock()) {
return this.write(src, this.size, maximum);
}
}
int writeAtEnd(ByteBuffer src) {
try (AutoRelease lock = this.writeLock()) {
return this.writeShort(src, this.size);
}
}
int writeAtEnd(byte[] src, int off, int len) {
try (AutoRelease lock = this.writeLock()) {
return this.write(src, this.size, off, len);
}
}
void truncate(long newSize) {
try (AutoRelease lock = this.writeLock()) {
if (newSize < this.size) {
this.size = newSize;
}
}
}
MemoryFileLock tryLock(MemoryFileLock lock) {
try (AutoRelease autoRelease = this.writeLock()) {
return this.lockSet().tryLock(lock);
}
}
MemoryFileLock lock(MemoryFileLock lock) throws IOException {
try (AutoRelease autoRelease = this.writeLock()) {
return this.lockSet().lock(lock);
}
}
void unlock(MemoryFileLock lock) {
try (AutoRelease autoRelease = this.writeLock()) {
this.lockSet.remove(lock);
}
}
private LockSet lockSet() {
if (this.lockSet == null) {
this.lockSet = new LockSet();
}
return this.lockSet;
}
// void accessed();
//
// void modified();
private byte[] getBlock(int currentBlock) {
if (currentBlock == 0) {
return this.directBlock;
} else {
return this.indirectBlocks[currentBlock - 1];
}
}
private static int writeFully(ByteBuffer src, WritableByteChannel target, int toWrite) throws IOException {
int written = 0;
while (written < toWrite) {
written += target.write(src);
}
return written;
}
private static int readFully(ByteBuffer src, ReadableByteChannel target, int toRead) throws IOException {
int read = 0;
while (read < toRead) {
read += target.read(src);
}
return read;
}
private void ensureCapacity(long capacity) {
// if direct block is enough do nothing
if (capacity <= BLOCK_SIZE) {
return;
}
// lazily allocate indirect blocks
if (this.indirectBlocks == null) {
this.indirectBlocks = new byte[NUMBER_OF_BLOCKS][];
}
int blocksRequired = (int) ((capacity - 1L)/ BLOCK_SIZE); // consider already present direct block, don't add + 1
if (blocksRequired > NUMBER_OF_BLOCKS) {
// FIXME implement double indirect addressing
throw new AssertionError("files bigger than 16MB not yet supported");
}
if (blocksRequired > this.indirectBlocksAllocated) {
for (int i = this.indirectBlocksAllocated; i < blocksRequired; ++i) {
this.indirectBlocks[i] = new byte[BLOCK_SIZE];
this.indirectBlocksAllocated += 1;
}
}
}
}