/*
* 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.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import uk.me.parabola.imgfmt.FileSystemParam;
import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.labelenc.CharacterEncoder;
import uk.me.parabola.imgfmt.app.labelenc.CodeFunctions;
import uk.me.parabola.imgfmt.app.labelenc.EncodedText;
import uk.me.parabola.imgfmt.fs.ImgChannel;
import uk.me.parabola.log.Logger;
import static java.util.Arrays.asList;
/**
* The header at the very beginning of the .img filesystem. It has the
* same signature as a DOS partition table, although I don't know
* exactly how much the partition concepts are used.
*
* @author Steve Ratcliffe
*/
class ImgHeader {
private static final Logger log = Logger.getLogger(ImgHeader.class);
// Offsets into the header.
private static final int OFF_XOR = 0x0;
private static final int OFF_UPDATE_MONTH = 0xa;
private static final int OFF_UPDATE_YEAR = 0xb; // +1900 for val >= 0x63, +2000 for less
private static final int OFF_SUPP = 0xe; // Appears to be set for gmapsupp files
private static final int OFF_CHECKSUM = 0xf;
private static final int OFF_SIGNATURE = 0x10;
private static final int OFF_UNK_1 = 0x17;
// If this was a real boot sector these would be the meanings
private static final int OFF_SECTORS = 0x18;
private static final int OFF_HEADS = 0x1a;
private static final int OFF_CYLINDERS = 0x1c;
private static final int OFF_CREATION_DATE = 0x39;
// The block number where the directory starts.
private static final int OFF_DIRECTORY_START_BLOCK = 0x40;
private static final int OFF_MAP_FILE_INTENTIFIER = 0x41;
private static final int OFF_MAP_DESCRIPTION = 0x49; // 0x20 padded
private static final int OFF_HEADS2 = 0x5d;
private static final int OFF_SECTORS2 = 0x5f;
private static final int OFF_BLOCK_SIZE_EXPONENT1 = 0x61;
private static final int OFF_BLOCK_SIZE_EXPONENT2 = 0x62;
private static final int OFF_BLOCK_SIZE = 0x63;
// private static final int OFF_UKN_3 = 0x63;
private static final int OFF_MAP_NAME_CONT = 0x65;
// 'Partition table' offsets.
private static final int OFF_START_HEAD = 0x1bf;
private static final int OFF_START_SECTOR = 0x1c0;
private static final int OFF_START_CYLINDER = 0x1c1;
private static final int OFF_SYSTEM_TYPE = 0x1c2;
private static final int OFF_END_HEAD = 0x1c3;
private static final int OFF_END_SECTOR = 0x1c4;
private static final int OFF_END_CYLINDER = 0x1c5;
private static final int OFF_REL_SECTORS = 0x1c6;
private static final int OFF_NUMBER_OF_SECTORS = 0x1ca;
private static final int OFF_PARTITION_SIG = 0x1fe;
// Lengths of some of the fields
private static final int LEN_MAP_NAME_CONT = 30;
private static final int LEN_MAP_DESCRIPTION = 20;
private FileSystemParam fsParams;
private final ByteBuffer header = ByteBuffer.allocate(512);
private ImgChannel file;
private Date creationTime;
private int sectorsPerTrack;
private int headsPerCylinder;
// Signatures.
private static final byte[] FILE_ID = {
'G', 'A', 'R', 'M', 'I', 'N', '\0'};
private static final byte[] SIGNATURE = {
'D', 'S', 'K', 'I', 'M', 'G', '\0'};
private int numBlocks;
ImgHeader(ImgChannel chan) {
this.file = chan;
header.order(ByteOrder.LITTLE_ENDIAN);
}
/**
* Create a header from scratch.
* @param params File system parameters.
*/
void createHeader(FileSystemParam params) {
this.fsParams = params;
header.put(OFF_XOR, (byte) 0);
// Set the block size. 2^(E1+E2) where E1 is always 9.
int exp = 9;
int bs = params.getBlockSize();
for (int i = 0; i < 32; i++) {
bs >>>= 1;
if (bs == 0) {
exp = i;
break;
}
}
if (exp < 9)
throw new IllegalArgumentException("block size too small");
header.put(OFF_BLOCK_SIZE_EXPONENT1, (byte) 0x9);
header.put(OFF_BLOCK_SIZE_EXPONENT2, (byte) (exp - 9));
header.position(OFF_SIGNATURE);
header.put(SIGNATURE);
header.position(OFF_MAP_FILE_INTENTIFIER);
header.put(FILE_ID);
header.put(OFF_UNK_1, (byte) 0x2);
// Actually this may not be the directory start block, I am guessing -
// always assume it is 2 anyway.
header.put(OFF_DIRECTORY_START_BLOCK, (byte) fsParams.getDirectoryStartEntry());
header.position(OFF_CREATION_DATE);
Utils.setCreationTime(header, creationTime);
setDirectoryStartEntry(params.getDirectoryStartEntry());
// Set the times.
Date date = new Date();
setCreationTime(date);
setUpdateTime(date);
setDescription(params.getMapDescription());
header.put(OFF_SUPP, (byte) (fsParams.isGmapsupp() && fsParams.isHideGmapsuppOnPC() ? 1: 0));
// Checksum is not checked.
header.put(OFF_CHECKSUM, (byte) 0);
}
/**
* Write out the values associated with the partition sizes.
*
* @param blockSize Block size.
*/
private void writeSizeValues(int blockSize) {
int endSector = (int) (((numBlocks+1L) * blockSize + 511) / 512);
//System.out.printf("end sector %d %x\n", endSector, endSector);
// We have three maximum values for sectors, heads and cylinders. We attempt to find values
// for them that are larger than the
sectorsPerTrack = 32; // 6 bit value
headsPerCylinder = 128;
int cyls = 0x400;
// Try out various values of h, s and c until we find a combination that is large enough.
// I'm not entirely sure about the valid values, but it seems that only certain values work
// which is why we use values from a list.
// See: http://www.win.tue.nl/~aeb/partitions/partition_types-2.html for justification for the h list
out:
for (int h : asList(16, 32, 64, 128, 256)) {
for (int s : asList(4, 8, 16, 32)) {
for (int c : asList(0x20, 0x40, 0x80, 0x100, 0x200, 0x3ff)) {
log.info("shc=", s + "," + h + "," + c, "end=", endSector);
//System.out.println("shc=" + s + "," + h + "," + c + "end=" + endSector);
if (s * h * c > endSector) {
headsPerCylinder = h;
sectorsPerTrack = s;
cyls = c;
break out;
}
}
}
}
// This sectors, head, cylinders stuff appears to be used by mapsource
// and they have to be larger than the actual size of the map. It
// doesn't appear to have any effect on a garmin device or other software.
header.putShort(OFF_SECTORS, (short) sectorsPerTrack);
header.putShort(OFF_SECTORS2, (short) sectorsPerTrack);
header.putShort(OFF_HEADS, (short) headsPerCylinder);
header.putShort(OFF_HEADS2, (short) headsPerCylinder);
header.putShort(OFF_CYLINDERS, (short) cyls);
// Since there are only 2 bytes here it can overflow, if it
// does we replace it with 0xffff.
int blocks = (int) (endSector * 512L / blockSize);
char shortBlocks = blocks > 0xffff ? 0xffff : (char) blocks;
header.putChar(OFF_BLOCK_SIZE, shortBlocks);
header.put(OFF_PARTITION_SIG, (byte) 0x55);
header.put(OFF_PARTITION_SIG + 1, (byte) 0xaa);
// Partition starts at zero. This is 0,0,1 in CHS terms.
header.put(OFF_START_HEAD, (byte) 0);
header.put(OFF_START_SECTOR, (byte) 1);
header.put(OFF_START_CYLINDER, (byte) 0);
header.put(OFF_SYSTEM_TYPE, (byte) 0);
// Now calculate the CHS address of the last sector of the partition.
CHS chs = new CHS(endSector - 1);
header.put(OFF_END_HEAD, (byte) (chs.h));
header.put(OFF_END_SECTOR, (byte) ((chs.s) | ((chs.c >> 2) & 0xc0)));
header.put(OFF_END_CYLINDER, (byte) (chs.c & 0xff));
// Write the LBA block address of the beginning and end of the partition.
header.putInt(OFF_REL_SECTORS, 0);
header.putInt(OFF_NUMBER_OF_SECTORS, endSector);
log.info("number of blocks", endSector - 1);
}
void setHeader(ByteBuffer buf) {
buf.flip();
header.put(buf);
byte exp1 = header.get(OFF_BLOCK_SIZE_EXPONENT1);
byte exp2 = header.get(OFF_BLOCK_SIZE_EXPONENT2);
log.debug("header exponent", exp1, exp2);
fsParams = new FileSystemParam();
fsParams.setBlockSize(1 << (exp1 + exp2));
fsParams.setDirectoryStartEntry(header.get(OFF_DIRECTORY_START_BLOCK));
StringBuffer sb = new StringBuffer();
sb.append(Utils.bytesToString(buf, OFF_MAP_DESCRIPTION, LEN_MAP_DESCRIPTION));
sb.append(Utils.bytesToString(buf, OFF_MAP_NAME_CONT, LEN_MAP_NAME_CONT));
fsParams.setMapDescription(sb.toString().trim());
byte h = header.get(OFF_END_HEAD);
byte sc1 = header.get(OFF_END_SECTOR);
byte sc2 = header.get(OFF_END_CYLINDER);
CHS chs = new CHS();
chs.setFromPartition(h, sc1, sc2);
int lba = chs.toLba();
log.info("partition sectors", lba);
// ... more to do
}
void setFile(ImgChannel file) {
this.file = file;
}
FileSystemParam getParams() {
return fsParams;
}
/**
* Sync the header to disk.
* @throws IOException If an error occurs during writing.
*/
public void sync() throws IOException {
setUpdateTime(new Date());
writeSizeValues(fsParams.getBlockSize());
header.rewind();
file.position(0);
file.write(header);
file.position(fsParams.getDirectoryStartEntry() * 512L);
}
/**
* Set the update time.
* @param date The date to use.
*/
protected void setUpdateTime(Date date) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
header.put(OFF_UPDATE_YEAR, toYearCode(cal.get(Calendar.YEAR)));
header.put(OFF_UPDATE_MONTH, (byte) (cal.get(Calendar.MONTH)+1));
}
/**
* Set the description. It is spread across two areas in the header.
*
* It appears that the description has to be in ascii.
*
* @param desc The description.
*/
protected void setDescription(String desc) {
// Force the description to be in ascii.
CodeFunctions funcs = CodeFunctions.createEncoderForLBL(0, 0);
CharacterEncoder encoder = funcs.getEncoder();
EncodedText enc = encoder.encodeText(desc);
byte[] ctext = enc.getCtext();
int len = enc.getLength() - 1;
if (len > 50)
throw new IllegalArgumentException("Description is too long (max 50)");
byte[] part1 = new byte[LEN_MAP_DESCRIPTION];
Arrays.fill(part1, (byte) ' ');
byte[] part2 = new byte[LEN_MAP_NAME_CONT];
Arrays.fill(part2, (byte) ' ');
if (ctext != null) {
if (len > LEN_MAP_DESCRIPTION) {
System.arraycopy(ctext, 0, part1, 0, LEN_MAP_DESCRIPTION);
System.arraycopy(ctext, LEN_MAP_DESCRIPTION, part2, 0, len - LEN_MAP_DESCRIPTION);
} else {
System.arraycopy(ctext, 0, part1, 0, len);
}
}
header.position(OFF_MAP_DESCRIPTION);
header.put(part1);
header.position(OFF_MAP_NAME_CONT);
header.put(part2);
header.put((byte) 0); // really?
}
/**
* Convert a string to a byte array.
* @param s The string
* @return A byte array.
*/
private static byte[] toByte(String s) {
// NB: what character set should be used?
return s.getBytes();
}
/**
* Convert to the one byte code that is used for the year.
* If the year is in the 1900, then subtract 1900 and add the result to 0x63,
* else subtract 2000.
* Actually looks simpler, just subtract 1900..
* @param y The year in real-world format eg 2006.
* @return A one byte code representing the year.
*/
private static byte toYearCode(int y) {
return (byte) (y - 1900);
}
protected void setDirectoryStartEntry(int directoryStartEntry) {
header.put(OFF_DIRECTORY_START_BLOCK, (byte) directoryStartEntry);
fsParams.setDirectoryStartEntry(directoryStartEntry);
}
protected void setCreationTime(Date date) {
this.creationTime = date;
}
public void setNumBlocks(int numBlocks) {
this.numBlocks = numBlocks;
}
public void hideGmapsuppOnPC (boolean b) {
header.put(OFF_SUPP, (byte) (fsParams.isGmapsupp() && b ? 1: 0));
}
/**
* Represent a block number in the chs format.
*
* Note that this class uses the headsPerCylinder and sectorsPerTrack values
* from the enclosing class.
*
* @see <a href="http://en.wikipedia.org/wiki/Logical_Block_Addressing">Logical block addressing</a>
*/
private class CHS {
private int h;
private int s;
private int c;
private CHS() {
}
public CHS(int lba) {
toChs(lba);
}
/**
* Calculate the CHS values from the the given logical block address.
* @param lba Input logical block address.
*/
private void toChs(int lba) {
h = (lba / sectorsPerTrack) % headsPerCylinder;
s = (lba % sectorsPerTrack) + 1;
c = lba / (sectorsPerTrack * headsPerCylinder);
}
/**
* Set from a partition table entry.
*
* The cylinder is 10 bits and is split between the top 2 bit of the sector
* value and its own byte.
*
* @param h The h value.
* @param sc1 The s value (6 bits) and top 2 bits of c.
* @param sc2 The bottom 8 bits of c.
*/
public void setFromPartition(byte h, byte sc1, byte sc2) {
this.h = h;
this.s = (sc1 & 0x3f) + ((sc2 >> 2) & 0xc0);
this.c = sc2 & 0xff;
}
public int toLba() {
return (c * headsPerCylinder + h) * sectorsPerTrack + (s - 1);
}
}
}