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