/*
* Copyright (C) 2006 Steve Ratcliffe
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* Author: Steve Ratcliffe
* Create date: 03-Dec-2006
*/
package uk.me.parabola.imgfmt.sys;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousCloseException;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.FileChannel;
import java.nio.channels.NonReadableChannelException;
import java.nio.channels.NonWritableChannelException;
import uk.me.parabola.imgfmt.fs.ImgChannel;
import uk.me.parabola.log.Logger;
/**
* The internal representation of a file in the file system. In use it
* should only be referred to by the {@link ImgChannel} interface.
*
* @author Steve Ratcliffe
*/
public class FileNode implements ImgChannel {
private static final Logger log = Logger.getLogger(FileNode.class);
private boolean open;
private boolean writeable;
private boolean readable;
private final FileChannel file;
private final BlockManager blockManager;
private final Dirent dirent;
// The position in this file
private long position;
private byte xorByte;
/**
* Creates a new file in the file system. You can treat this just like
* a regular file and write or read from it.
* Operations to two different files may not be interleaved although
* it may be possible to implement this.
*
* @param file The handle to the underlying file.
* @param dir The directory entry associated with this file.
* @param mode The mode "rw" for read and write etc.
*/
public FileNode(FileChannel file, Dirent dir, String mode)
{
this.file = file;
this.dirent = dir;
if (mode.indexOf('r') >= 0)
readable = true;
if (mode.indexOf('w') >= 0)
writeable = true;
if (!(readable || writeable))
throw new IllegalArgumentException("File must be readable or writeable");
blockManager = dir.getBlockManager();
if (blockManager == null)
throw new IllegalArgumentException("no file system supplied");
open = true;
}
/**
* Closes this channel.
* <p/>
* <p> After a channel is closed, any further attempt to invoke I/O
* operations upon it will cause a {@link ClosedChannelException} to be
* thrown.
* <p/>
* <p> If this channel is already closed then invoking this method has no
* effect.
* <p/>
* <p> This method may be invoked at any time. If some other thread has
* already invoked it, however, then another invocation will block until
* the first invocation is complete, after which it will return without
* effect. </p>
*
* @throws IOException If an I/O error occurs
*/
public void close() throws IOException {
if (!open)
return;
sync();
open = false;
readable = false;
writeable = false;
}
/**
* Tells whether or not this channel is open. </p>
*
* @return <tt>true</tt> if, and only if, this channel is open
*/
public boolean isOpen() {
return open;
}
/**
* Reads a sequence of bytes from this channel into the given buffer.
*
* @param dst The buffer into which bytes are to be transferred
*
* @return The number of bytes read, possibly zero, or <tt>-1</tt> if the
* channel has reached end-of-stream
*
* @throws NonReadableChannelException If this channel was not opened for reading
* @throws ClosedChannelException If this channel is closed
* @throws AsynchronousCloseException If another thread closes this channel
* while the read operation is in progress
* @throws ClosedByInterruptException If another thread interrupts the
* current thread while the read operation is in progress, thereby closing
* the channel and setting the current thread's interrupt status
* @throws IOException If some other I/O error occurs
*/
public int read(ByteBuffer dst) throws IOException {
if (!open)
throw new ClosedChannelException();
if (!readable)
throw new NonReadableChannelException();
int blockSize = blockManager.getBlockSize();
long size = dst.remaining();
long fileSize = dirent.getSize();
if (position >= fileSize)
return -1;
size = Math.min(size, fileSize - position);
int totalRead = 0;
while (size > 0) {
// Tet the logical block number, as we see it in our file.
int lblock = (int) (position / blockSize);
// Get the physical block number, the actual block number in
// the underlying file.
int pblock = dirent.getPhysicalBlock(lblock);
if (pblock == 0xffff) {
// We are at the end of the file.
log.debug("at eof");
break;
}
// Position the underlying file
int off = (int) (position - lblock*blockSize);
file.position((long) pblock * blockSize + off);
int n = (int) size;
if (n > blockSize)
n = blockSize;
if (off != 0)
n = Math.min(n, blockSize - off);
dst.limit(dst.position() + n);
int pos = dst.position();
int nr = file.read(dst);
if (nr == -1)
return -1;
if (nr == 0)
throw new IOException("Read nothing");
if(xorByte != 0) {
byte[] bufBytes = dst.array();
for(int i = pos + n - 1; i >= pos; --i)
bufBytes[i] ^= xorByte;
}
// Update the file positions
size -= nr;
position += nr;
totalRead += nr;
}
log.debug("read ret", totalRead);
return totalRead;
}
/**
* Writes a sequence of bytes to this channel from the given buffer.
* <p/>
* <p> An attempt is made to write up to <i>r</i> bytes to the channel,
* where <i>r</i> is the number of bytes remaining in the buffer, that is,
* <tt>dst.remaining()</tt>, at the moment this method is invoked.
* <p>The logical block has to be converted to a physical block in the
* underlying file.
*
* @param src The buffer from which bytes are to be retrieved
* @return The number of bytes written, possibly zero
* @throws NonWritableChannelException
* If this channel was not opened for writing
* @throws ClosedChannelException
* If this channel is closed
* @throws IOException If some other I/O error occurs
*/
public int write(ByteBuffer src) throws IOException {
if (!open)
throw new ClosedChannelException();
int blockSize = blockManager.getBlockSize();
// Get the size of this write
int size = src.remaining();
// Loop over each block, this is to support the case (which we may
// not implement) of non-contiguous blocks.
int totalWritten = 0;
while (size > 0) {
// Get the logical block, ie the block as we see it in our file.
int lblock = (int) (position/blockSize);
// First need to allocate enough blocks for this write. First check
// if the block exists already
int pblock = dirent.getPhysicalBlock(lblock);
log.debug("lblock / pblock", lblock, '/', pblock);
if (pblock == 0xffff) {
log.debug("allocating new block");
pblock = blockManager.allocate();
dirent.addBlock(pblock);
}
// Position the underlying file, so that it is in the correct place.
int off = (int) (position - lblock*blockSize);
file.position((long) pblock * blockSize + off);
int n = size;
if (n > blockSize)
n = blockSize;
if (off != 0)
n = Math.min(n, blockSize - off);
src.limit(src.position() + n);
// Write to the underlying file.
int nw = file.write(src);
if (nw == 0)
throw new IOException("Wrote nothing");
// Update the file positions
size -= nw;
position += nw;
totalWritten += nw;
// Update file size.
if (position > dirent.getSize())
dirent.setSize((int) position);
}
return totalWritten;
}
public long position() {
return position;
}
public void position(long pos) {
int blockSize = blockManager.getBlockSize();
while (pos > position) {
long lblock = position / blockSize;
int pblock = dirent.getPhysicalBlock((int) lblock);
if (pblock == 0xffff) {
if (writeable) {
log.debug("setting position allocating new block", lblock);
pblock = blockManager.allocate();
dirent.addBlock(pblock);
}
}
position = (lblock+1) * blockSize;
}
this.position = pos;
}
/**
* Write out any unsaved data to disk.
*
* @throws IOException If there is an error writing to disk.
*/
private void sync() throws IOException {
if (!writeable)
return;
// Ensure that a complete block is written out.
int bs = blockManager.getBlockSize();
long rem = bs - (file.position() % bs);
ByteBuffer buf = ByteBuffer.allocate(blockManager.getBlockSize());
// Complete any partial block.
for (int i = 0; i < rem; i++)
buf.put((byte) 0);
buf.flip();
file.write(buf);
}
public void setXorByte(byte xorByte) {
this.xorByte = xorByte;
}
}