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