Subversion Repositories mkgmap

Rev

Rev 4823 | 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.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.nio.file.OpenOption;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
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;

import static uk.me.parabola.imgfmt.fs.DirectoryEntry.ENTRY_SIZE;
import static uk.me.parabola.imgfmt.fs.DirectoryEntry.SLOTS_PER_ENTRY;

/**
 * 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 = "        .   ";
        private static final OpenOption[] OPEN_CREATE_RW = {StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE};

        // 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;
        private FileSystemParam fsparam;

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

        // Open files for write
        private final List<FileNode> openNodes = new ArrayList<>();

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

        /**
         * 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 {
                        FileChannel chan = FileChannel.open(Paths.get(filename), OPEN_CREATE_RW);
                        return createFs(chan, params);
                } catch (IOException e) {
                        throw new FileNotWritableException("Could not create file", 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 {
                try {
                        FileChannel chan = FileChannel.open(Paths.get(name), StandardOpenOption.READ);
                        return openFs(name, chan);
                } catch (IOException e) {
                        throw new FileNotFoundException("Failed to create or open file " + name);
                }
        }

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

                FileNode node = new FileNode(file, dir, "w");
                openNodes.add(node);
                return node;
        }

        /**
         * 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");
                                }
                        }
                        FileNode node = new FileNode(file, ent, "w");
                        openNodes.add(node);
                        return node;
                } 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();
        }

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

                assert fileBlockManager.getMaxBlockAllocated() == 0;

                FileSystemParam param = fsparam;
                int totalBlocks = calcBlockParam(param);

                fileBlockManager.setBlockSize(param.getBlockSize());
                headerBlockManager.setBlockSize(param.getBlockSize());
                file.position((long)param.getReservedDirectoryBlocks() * param.getBlockSize());

                fileBlockManager.setCurrentBlock(param.getReservedDirectoryBlocks());
                for (FileNode n : openNodes) {
                        n.close();
                }

                header.createHeader(param);
                header.setNumBlocks(totalBlocks);
                header.sync();
                directory.sync();
        }

        /**
         * Calculate the block size and related parameters.
         *
         * We need to know the block size and the size of the directory before writing any of the files.
         */

        private int calcBlockParam(FileSystemParam param) {
                int bestBlockSize = 0;
                int reserved = 0;
                int sizeInBlocks = 0;
                long bestSize = Long.MAX_VALUE;

                for (int blockSize = param.getBlockSize(); blockSize < (1 << 24); blockSize <<= 1) {
                        int headerSlotsRequired = 1;  // for the top level directory entry
                        int fileBlocks = 0;

                        for (FileNode fn : openNodes) {
                                long len = fn.getSize();

                                // Blocks required for this file
                                int nBlocks = (int) ((len + blockSize - 1) / blockSize);
                                fileBlocks += nBlocks;

                                // Now we calculate how many directory blocks we need, you have
                                // to round up as files do not share directory blocks.
                                headerSlotsRequired += (nBlocks + SLOTS_PER_ENTRY - 1) / SLOTS_PER_ENTRY;
                        }

                        // Header blocks include the blocks before the directory.
                        int requiredSlots = param.getDirectoryStartEntry() + headerSlotsRequired;
                        int headerBlocks = (requiredSlots * ENTRY_SIZE + blockSize - 1) / blockSize;
                        int totalBlocks = headerBlocks + fileBlocks;
                        long size = (long) totalBlocks * blockSize;
                        log.infof("bs=%d, whole size=%d, hb=%d, fb=%d, blocks=%d", blockSize, size,
                                        headerBlocks, fileBlocks, totalBlocks);

                        if (headerBlocks > SLOTS_PER_ENTRY)
                                continue;

                        if (totalBlocks > 0xfffe)
                                continue;

                        if (size > bestSize)
                                break;

                        bestBlockSize = blockSize;
                        reserved = headerBlocks;
                        sizeInBlocks = fileBlocks + headerBlocks;
                        bestSize = size;
                }
                log.infof("Best block size: %d sizeInBlocks=%d, reserved=%d", bestBlockSize, sizeInBlocks, reserved);

                param.setBlockSize(bestBlockSize);
                param.setReservedDirectoryBlocks(reserved);
                return sizeInBlocks;
        }

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

        public FileSystemParam fsparam() {
                return fsparam;
        }

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

                this.fsparam = params;

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

                        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);
                fsparam = header.getParams();

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

                directory = new Directory(headerBlockManager);
                directory.setStartPos(fsparam.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;
        }
}