/*
* 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_VERSION = 0x8
;
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());
if (fsParams.
isGmapsupp()) {
header.
put(OFF_SUPP,
(byte) (fsParams.
isHideGmapsuppOnPC() ? 1:
0));
int prodVersion = fsParams.
getProductVersion();
if (prodVersion
>=
0) {
// value 100 means 1.00 */
int major = prodVersion /
100;
int minor = prodVersion
% 100;
short version =
(short) (major |
(minor
<< 8));
header.
putShort(OFF_VERSION, version
);
}
}
// 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);
// 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
);
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 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);
}
}
}