Subversion Repositories mkgmap

Rev

Rev 2294 | 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: 26-Nov-2006
 */

package uk.me.parabola.imgfmt.sys;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.util.List;

import uk.me.parabola.imgfmt.FileExistsException;
import uk.me.parabola.imgfmt.FileNotWritableException;
import uk.me.parabola.imgfmt.FileSystemParam;
import uk.me.parabola.imgfmt.fs.DirectoryEntry;
import uk.me.parabola.imgfmt.fs.FileSystem;
import uk.me.parabola.imgfmt.fs.ImgChannel;
import uk.me.parabola.log.Logger;

/**
 * The img file is really a filesystem containing several files.
 * It is made up of a header, a directory area and a data area which
 * occur in the filesystem in that order.
 *
 * @author steve
 */

public class ImgFS implements FileSystem {
        private static final Logger log = Logger.getLogger(ImgFS.class);

        // The directory is just like any other file, but with a name of 8+3 spaces
        static final String DIRECTORY_FILE_NAME = "        .   ";

        // This is the read or write channel to the real file system.
        private final FileChannel file;
        private boolean readOnly = true;

        // The header contains general information.
        private ImgHeader header;

        // There is only one directory that holds all filename and block allocation
        // information.
        private Directory directory;

        // The filesystem is responsible for allocating blocks
        private BlockManager fileBlockManager;

        // The header entries are written in 512 blocks, regardless of the block size of the file itself.
        private static final long ENTRY_BLOCK_SIZE = 512L;
        private BlockManager headerBlockManager;

        private byte xorByte;   // if non-zero, all bytes are XORed with this

        /**
         * Private constructor, use the static {@link #createFs} and {@link #openFs}
         * routines to make a filesystem.
         *
         * @param chan The open file.
         */

        private ImgFS(FileChannel chan) {
                file = chan;
        }

        /**
         * Create an IMG file from its external filesystem name and optionally some
         * parameters.
         *
         * @param filename The name of the file to be created.
         * @param params File system parameters.  Can not be null.
         * @throws FileNotWritableException If the file can not be written to.
         */

        public static FileSystem createFs(String filename, FileSystemParam params) throws FileNotWritableException {
                params.setFilename(filename);
                try {
                        RandomAccessFile rafile = new RandomAccessFile(filename, "rw");
                        return createFs(rafile.getChannel(), params);
                } catch (FileNotFoundException e) {
                        throw new FileNotWritableException("Could not create file: " + params.getFilename(), e);
                }
        }

        private static FileSystem createFs(FileChannel chan, FileSystemParam params)
                        throws FileNotWritableException
        {
                assert params != null;

                // Truncate the file, because extra bytes beyond the end make for a
                // map that doesn't work on the GPS (although its likely to work in
                // other software viewers).
                try {
                        chan.truncate(0);
                } catch (IOException e) {
                        throw new FileNotWritableException("Failed to truncate file", e);
                }

                ImgFS fs = new ImgFS(chan);
                fs.createInitFS(chan, params);

                return fs;
        }

        /**
         * Open an existing IMG file system.
         * @param name The file name to open.
         * @return A File system that can be used lookup the internal files.
         * @throws FileNotFoundException When the file doesn't exist or can't be
         * read.
         */

        public static FileSystem openFs(String name) throws FileNotFoundException {
                RandomAccessFile rafile = new RandomAccessFile(name, "r");
                return openFs(name, rafile.getChannel());
        }

        private static FileSystem openFs(String name, FileChannel chan) throws FileNotFoundException {
                ImgFS fs = new ImgFS(chan);

                try {
                        fs.readInitFS(chan);
                } catch (IOException e) {
                        throw new FileNotFoundException(name + ": " + e.getMessage());
                }

                return fs;
        }

        /**
         * Create a new file, it must not already exist.
         *
         * @param name The file name.
         * @return A directory entry for the new file.
         */

        public ImgChannel create(String name) throws FileExistsException {
                Dirent dir = directory.create(name, fileBlockManager);

                return new FileNode(file, dir, "w");
        }

        /**
         * Open a file.  The returned file object can be used to read and write the
         * underlying file.
         *
         * @param name The file name to open.
         * @param mode Either "r" for read access, "w" for write access or "rw"
         *             for both read and write.
         * @return A file descriptor.
         * @throws FileNotFoundException When the file does not exist.
         */

        public ImgChannel open(String name, String mode) throws FileNotFoundException {
                if (name == null || mode == null)
                        throw new IllegalArgumentException("null argument");

                if (mode.indexOf('r') >= 0) {
                        Dirent ent = internalLookup(name);

                        FileNode fn = new FileNode(file, ent, "r");
                        if(xorByte != 0)
                                fn.setXorByte(xorByte);
                        return fn;
                } else if (mode.indexOf('w') >= 0) {
                        Dirent ent;
                        try {
                                ent = internalLookup(name);
                        } catch (FileNotFoundException e) {
                                try {
                                        ent = directory.create(name, fileBlockManager);
                                } catch (FileExistsException e1) {
                                        // This shouldn't happen as we have just checked.
                                        throw new FileNotFoundException("Attempt to duplicate a file name");
                                }
                        }
                        return new FileNode(file, ent, "w");
                } else {
                        throw new IllegalArgumentException("Invalid mode given");
                }
        }

        /**
         * Lookup the file and return a directory entry for it.
         *
         * @param name The filename to look up.
         * @return A directory entry.
         * @throws FileNotFoundException If an error occurs looking for the file,
         * including it not existing.
         */

        public DirectoryEntry lookup(String name) throws FileNotFoundException {
                return internalLookup(name);
        }

        /**
         * List all the files in the directory.
         *
         * @return A List of directory entries.
         */

        public List<DirectoryEntry> list()  {
                return directory.getEntries();
        }

        public FileSystemParam fsparam() {
                return header.getParams();
        }

        public void fsparam(FileSystemParam param) {
                int reserved = param.getReservedDirectoryBlocks() + 2;
                fileBlockManager.setCurrentBlock(reserved);
                headerBlockManager.setMaxBlock(reserved);
        }

        /**
         * Sync with the underlying file.  All unwritten data is written out to
         * the underlying file.
         *
         * @throws IOException If an error occurs during the write.
         */

        public void sync() throws IOException {
                if (readOnly)
                        return;

                header.setNumBlocks(fileBlockManager.getMaxBlockAllocated());
                header.sync();
                directory.sync();
        }

        /**
         * Close the filesystem.  Any saved data is flushed out.  It is better
         * to explicitly sync the data out first, to be sure that it has worked.
         */

        public void close() {

                try {
                        sync();
                } catch (IOException e) {
                        log.debug("could not sync filesystem");
                } finally {
                        try {
                                file.close();
                        } catch (IOException e) {
                                log.warn("Could not close file");
                        }
                }
        }

        /**
         * Set up and ImgFS that has just been created.
         *
         * @param chan The real underlying file to write to.
         * @param params The file system parameters.
         * @throws FileNotWritableException If the file cannot be written for any
         * reason.
         */

        private void createInitFS(FileChannel chan, FileSystemParam params) throws FileNotWritableException {
                readOnly = false;

                // The block manager allocates blocks for files.
                headerBlockManager = new BlockManager(params.getBlockSize(), 0);
                headerBlockManager.setMaxBlock(params.getReservedDirectoryBlocks());

                // This bit is tricky.  We want to use a regular ImgChannel to write
                // to the header and directory, but to create one normally would involve
                // it already existing, so it is created by hand.
                try {
                        directory = new Directory(headerBlockManager, params.getDirectoryStartEntry());

                        Dirent ent = directory.create(DIRECTORY_FILE_NAME, headerBlockManager);
                        ent.setSpecial(true);
                        ent.setInitialized(true);

                        FileNode f = new FileNode(chan, ent, "w");

                        directory.setFile(f);
                        header = new ImgHeader(f);
                        header.createHeader(params);
                } catch (FileExistsException e) {
                        throw new FileNotWritableException("Could not create img file directory", e);
                }

                fileBlockManager = new BlockManager(params.getBlockSize(), params.getReservedDirectoryBlocks());

                assert header != null;
        }

        /**
         * Initialise a filesystem that is going to be read from.  We need to read
         * in the header including directory.
         *
         * @param chan The file channel to read from.
         * @throws IOException If the file cannot be read.
         */

        private void readInitFS(FileChannel chan) throws IOException {
                ByteBuffer headerBuf = ByteBuffer.allocate(512);
                headerBuf.order(ByteOrder.LITTLE_ENDIAN);
                chan.read(headerBuf);
                xorByte = headerBuf.get(0);
                if(xorByte != 0) {
                        byte[] headerBytes = headerBuf.array();
                        for(int i = 0; i < headerBytes.length; ++i)
                                headerBytes[i] ^= xorByte;
                }

                if (headerBuf.position() < 512)
                        throw new IOException("File too short or corrupted");

                header = new ImgHeader(null);
                header.setHeader(headerBuf);
                FileSystemParam params = header.getParams();

                BlockManager headerBlockManager = new BlockManager(params.getBlockSize(), 0);
                headerBlockManager.setMaxBlock(params.getReservedDirectoryBlocks());

                directory = new Directory(headerBlockManager, params.getDirectoryStartEntry());
                directory.setStartPos(params.getDirectoryStartEntry() * ENTRY_BLOCK_SIZE);

                Dirent ent = directory.create(DIRECTORY_FILE_NAME, headerBlockManager);
                FileNode f = new FileNode(chan, ent, "r");

                header.setFile(f);
                directory.setFile(f);
                directory.readInit(xorByte);
        }

        /**
         * Lookup the file and return a directory entry for it.
         *
         * @param name The filename to look up.
         * @return A directory entry.
         * @throws FileNotFoundException If an error occurs reading the directory.
         */

        private Dirent internalLookup(String name) throws FileNotFoundException {
                if (name == null)
                        throw new IllegalArgumentException("null name argument");

                Dirent ent = (Dirent) directory.lookup(name);
                if (ent == null)
                        throw new FileNotFoundException(name + " not found");

                return ent;
        }

}