Subversion Repositories mkgmap

Rev

Rev 1226 | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * 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.Closeable;
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.Sized;
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, FileLink {
        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;
        private Closeable outerClose;

        /**
         * 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;

                if (writeable && outerClose != null)
                        outerClose.close();

                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;
        }

        public void link(Sized source, Closeable closeable) {
                dirent.setSizeSource(source);
                outerClose = closeable;
        }

        public long getSize() {
                return dirent.getSize();
        }

        /**
         * 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;
        }

        public String toString() {
                return String.format("%s %d", dirent.getFullName(), getSize());
        }
}