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