Skip to content

Decode only decodes first field of byte array #33

@hengxti

Description

@hengxti

I have a standard use case where I decode a custom class, but only the first field of my class id decoded correctly. All other fields remain on default values.
Here is my custum class:

package fat.structures;

import java.io.UnsupportedEncodingException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;

import org.codehaus.preon.annotation.BoundString;
import org.codehaus.preon.annotation.BoundString.Encoding;

public class DirectoryEntry {

private static final int MAXIMAL_FILE_NAME_LENGTH = 11;

/**
 * @see setFATDate 
 * @see setFATTime
 */

// first bit 1 is equivalent to '-', , due to the tow's complement in Short. Note java does not offer unsigned short :/
private static final short HOUR_BIT_MASK   = Short.parseShort("-111 1000 0000 0000".replaceAll("\\s+",""),2);
private static final short MINUTE_BIT_MASK = Short.parseShort("0000 0111 1110 0000".replaceAll("\\s+",""),2);
private static final short SECOND_BIT_MASK = Short.parseShort("0000 0000 0001 1111".replaceAll("\\s+",""),2);
private static final short YEAR_BIT_MASK   = Short.parseShort("-111 1110 0000 0000".replaceAll("\\s+",""),2);
private static final short MONTH_BIT_MASK  = Short.parseShort("0000 0001 1110 0000".replaceAll("\\s+",""),2);
private static final short DAY_BIT_MASK    = Short.parseShort("0000 0000 0001 1111".replaceAll("\\s+",""),2);

public static final byte DELETED_MARK = (byte) 0xE5;
public static final byte UNALLOCATED_MARK = (byte) 0x00;

private static final short MINIMUM_DATE = 0x2100;

public static final int SIZE_BYTES = 32;

/**
 * ATTR_READ_ONLY   Indicates that writes to the file should fail.
 * ATTR_HIDDEN  Indicates that normal directory listings should not show this file. 
 * ATTR_SYSTEM  Indicates that this is an operating system file.
 * ATTR_VOLUME_ID   There should only be one “file” on the volume that has this attribute set, and that file
 *  must be in the root directory. This name of this file is actually the label for the volume. DIR_FstClusHI 
 *  and DIR_FstClusLO must always be 0 for the volume label (no data clusters are allocated to the volume label file). 
 * ATTR_DIRECTORY   Indicates that this file is actually a container for other files.
 * ATTR_ARCHIVE     This attribute supports backup utilities. This bit is set by the FAT file system driver 
 * when a file is created, renamed, or written to. Backup utilities may use this attribute to indicate which 
 * files on the volume have been modified since the last time that a backup was performed.
 */
public static enum Attribute{
    READ_ONLY ((byte)0x01),
    HIDDEN ((byte)0x02),
    SYSTEM ((byte)0x04),
    VOLUME_ID ((byte)0x08),
    DIRECTORY ((byte)0x10),
    ARCHIVE ((byte)0x20),
    @Deprecated
    LONG_NAME ((byte)0x00); // set the two top most bits 0 and never use them again
    private final byte val;
    private Attribute(byte val) {
        this.val = val;
    }
    public byte getVal(){
        return val;
    }
};

/**
 * This is the short file name (SFN) which represents the name and the extension. 11 characters long, padded.
 * Here are some examples of how a user-entered name maps into DIR_Name:
 *
 * “foo.bar”            -> “FOO     BAR”
 * “FOO.BAR”            -> “FOO     BAR”
 * “Foo.Bar”            -> “FOO     BAR”
 * “foo”                -> “FOO        “
 * “foo.”               -> “FOO        “
 * “PICKLE.A”           -> “PICKLE  A  “
 * “prettybg.big”       -> “PRETTYBGBIG”
 * “.big”               -> illegal, DIR_Name[0] cannot be 0x20
 *
 */
@BoundString(size="11", encoding=Encoding.ISO_8859_1)
private String fileName = " ";

@BoundString(size="8")
private byte attributes; 

/**
 *  This is 0 and never used.
 */
@BoundString(size="8")
@Deprecated
private byte windowsNTReserved = 0; 

/**
 * Millisecond stamp at file creation time. This field actually contains a count of tenths of a second.
 * The granularity of the seconds part of DIR_CrtTime is 2 seconds so this field is a count of tenths of a 
 * second and its valid value range is 0-199 inclusive.
 * 10 000 000 ns = 10 ms
 */
@BoundString(size="8")
private byte createTimeMSstamp;  

@BoundString(size="16")
private short createTime; 

@BoundString(size="16")
private short createDate=MINIMUM_DATE;

/**
 * Last access date. Note that there is no last access time, only a date. This is the date of 
 * last read or write. In the case of a write, this should be set to the same date as DIR_WrtDate.
 */
@BoundString(size="16")
private short lastAccessDate= MINIMUM_DATE; 

/**
 * High word of this entry’s first cluster number (always 0 for a FAT12 or FAT16 volume).
 */
@BoundString(size="16")
@Deprecated
private short firstClusterHighByte = 0;

/**
 * Time of last write. Note that file creation is considered a write.
 */
@BoundString(size="16")
private short writeTime; 

/**
 * Date of last write. Note that file creation is considered a write.
 */
@BoundString(size="16")
private short writeDate=MINIMUM_DATE;

/**
 * This address is to be used in the FAT. Also firstClusterLowByte
 */
@BoundString(size="16")
private short startCluster;

/**
 * File size in bytes
 */
@BoundString(size="32")
private int fileSize;

// long file name only available in FAT 32

public DirectoryEntry(){

}

public DirectoryEntry(String fileName, short startCluster, int fileSize, Attribute...attributes) {

    setFileName(fileName);
    setAttributes(attributes);

    LocalDateTime now = LocalDateTime.now();
    if(now.getSecond() % 2 == 1){ // seconds odd?
        this.createTimeMSstamp = (byte) (now.getNano()/10000000 +100);
    }else{
        this.createTimeMSstamp = (byte) (now.getNano()/10000000);
    }
    this.createDate = setFATDate(now);
    this.createTime = setFATTime(now);
    this.lastAccessDate = setFATDate(now);
    this.writeDate = setFATDate(now);
    this.writeTime = setFATTime(now);

    this.startCluster = startCluster;
    this.fileSize = fileSize;
}

/**
 * Time Format. A FAT directory entry time stamp is a 16-bit field that
 * has a granularity of 2 seconds. Here is the format (bit 0 is the LSB
 * of the 16-bit word, bit 15 is the MSB of the 16-bit word).
 * 
 * Bits 0–4: 2-second count, valid value range 0–29 inclusive (0 – 58
 *  seconds). 
 * Bits 5–10: Minutes, valid value range 0–59 inclusive. 
 * Bits 11–15: Hours, valid value range 0–23 inclusive.
 * 
 * The valid time range is from Midnight 00:00:00 to 23:59:58.
 */
private short setFATTime(LocalDateTime dateTime) {
    short time = 0;
    time |= dateTime.getSecond()/2; // Notice this is a integer division the remainder is lost -> automatic round down.
    time |= dateTime.getMinute()<<5;
    time |= dateTime.getHour()<<11;
    return time;
}

/**
 * 
 * Date Format. A FAT directory entry date stamp is a 16-bit field that
 * is basically a date relative to the MS-DOS epoch of 01/01/1980. Here
 * is the format (bit 0 is the LSB of the 16-bit word, bit 15 is the MSB
 * of the 16-bit word):
 * 
 * Bits 0–4: Day of month, valid value range 1-31 inclusive. 
 * Bits 5–8: Month of year, 1 = January, valid value range 1–12 inclusive. 
 * Bits 9–15: Count of years from 1980, valid value range 0–127 inclusive
 * (1980–2107).
 */
private short setFATDate(LocalDateTime dateTime) {
    short date = 0;
    date |= dateTime.getDayOfMonth()-1;
    date |= dateTime.getMonthValue()<<5;
    date |= (dateTime.getYear()-1980)<<9;
    return date;
}

/**
 * @return the fileName
 */
public String getFileName() {
    return fileName;
}

/**
 * @param fileName including extension 
 */
public void setFileName(String fileName) {
    if(fileName == null || fileName.length()==0 || fileName.length()>MAXIMAL_FILE_NAME_LENGTH|| islegalShortFileName(fileName)){
        throw new IllegalArgumentException("Bad file name :/");
    }
    assert fileName.length() <=MAXIMAL_FILE_NAME_LENGTH;
    //TODO cut off filename nicely with ~
    while(fileName.length()>MAXIMAL_FILE_NAME_LENGTH){ // padding
        fileName +=" ";
    }
    this.fileName = fileName;
}

/**
 * a valid FAT16 short file name ...
 * ... can contain upper case letters A–Z
 * ... can contain numbers 0-9
 * TODO ... can contain ! # $ % & ' ( ) - @ ^ _ ` { } ~
 * TODO ... can contain special ASCII character 128-155
 * TODO ... can contain space due to padding, but can NOT start with a space (0x20)
 * @param fileName2
 * @return
 */
private boolean islegalShortFileName(String fileName2) {
    String pattern = "[A-Z]|[0-9]";
    //String pattern = "[A-Z]|[0-9]|\!|\#|\$|\%|\&|\'|\(|\)|\-|\@|\^|\_|\`|\{|\}|\~";
    return fileName2.matches(pattern);
}

/**
 * @return the fileExtention
 */
public String getFileExtention() {
    return fileName.substring(7, 10);
}

/**
 * Set the file name first then the extension!
 * @param fileExtention the fileExtention to set
 */
public void setFileExtention(String fileExtention) {
    this.fileName.substring(8, 11);
}

/**
 * @return the attributes
 */
public boolean isAttributeSet(Attribute attribute) {
    return (attributes&attribute.getVal())==attribute.getVal();
}

/**
 * @param attributes the attributes to set
 */
public void setAttributes(Attribute...attributes) {
    if (attributes == null) {
        throw new IllegalArgumentException("invalid attributes");
    }
    //Note that there is no security check whatsoever
    for(Attribute att:attributes){
        this.attributes|=att.getVal(); 
    }
}

/**
 * @return the createTime
 */
public LocalTime getCreateTime() {
    LocalTime lt = getTime(createTime);
    lt.plusNanos(createTimeMSstamp*10000000);
    return lt;
}

private LocalTime getTime(short time) {
    LocalTime lt = LocalTime.of( (time & HOUR_BIT_MASK) >>>11, (time & MINUTE_BIT_MASK) >>>5, (time & SECOND_BIT_MASK));
    return lt;
}

/**
 * @return the createDate
 */
public LocalDate getCreateDate() {
    return getFATDate(this.createDate);
}

private LocalDate getFATDate(short date) {
    LocalDate ld = LocalDate.of((date & YEAR_BIT_MASK)>>>9 + 1980, (date & MONTH_BIT_MASK)>>>5, (date & DAY_BIT_MASK)+1);
    return ld;
}

/**
 * @return the lastAccessDate
 */
public LocalDate getLastAccessDate() {
    return getFATDate(this.lastAccessDate);
}

/**
 * @param lastAccessDate the lastAccessDate to set
 */
public void setLastAccessDateToNow() {
    this.lastAccessDate = setFATDate(LocalDateTime.now());
}

/**
 * @return the writeDate
 */
public LocalDate getWriteDate() {
    return getFATDate(this.writeDate);
}

/**
 * @param writeDate the writeDate to set
 */
public void setWriteDateToNow() {
    this.writeDate = setFATDate(LocalDateTime.now());
}

/**
 * @return the startCluster
 */
public short getStartCluster() {
    return startCluster;
}

/**
 * @param startCluster the startCluster to set
 */
public void setStartCluster(short startCluster) {
    this.startCluster = startCluster;
}

/**
 * @return the fileSize
 */
public int getFileSize() {
    return fileSize;
}

/**
 * @param fileSize the fileSize to set
 */
public void setFileSize(int fileSize) {
    this.fileSize = fileSize;
}

public void setDeleted (){
    setFirstByte(DirectoryEntry.DELETED_MARK);
}
public boolean isDeleted(){
    return this.fileName.getBytes()[0] == DirectoryEntry.DELETED_MARK;
}

public void setUnallocated(){
    setFirstByte(DirectoryEntry.UNALLOCATED_MARK);
}

public boolean isUnallocated(){
    if(this.fileName.getBytes().length ==0){
        return true;
    }
    return this.fileName.getBytes()[0] == DirectoryEntry.UNALLOCATED_MARK;
}

private void setFirstByte(byte magicNumber) {
    byte[] fileNameBytes = this.fileName.getBytes();
    fileNameBytes[0] = magicNumber;

    try {
        this.fileName = new String(fileNameBytes, Encoding.ISO_8859_1.toString());
    } catch (UnsupportedEncodingException e) {
        System.err.println("Encoding not supported.");
        e.printStackTrace();
        System.exit(1);
    }
}

/* (non-Javadoc)
 * @see java.lang.Object#toString()
 */
@Override
public String toString() {
    return "DirectoryEntry [fileName=" + fileName + ", attributes="
            + attributes + ", createTimeMSstamp=" + createTimeMSstamp
            + ", createTime=" + createTime + ", createDate=" + createDate
            + ", lastAccessDate=" + lastAccessDate + ", writeTime="
            + writeTime + ", writeDate=" + writeDate + ", startCluster="
            + startCluster + ", fileSize=" + fileSize + "]";
}

}

Here is my test code:

private static DirectoryEntry[] readConsecutiveDirectoryCluster(FAT16 fat16,
int directoryStart, int directorySize) throws IOException, DecodingException {
Codec directoryEntryCodec = Codecs.create(DirectoryEntry.class);
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
// File handler = new File(".\directoryEntry.html");
// Codecs.document(directoryEntryCodec, DocumentType.Html, handler);

    for (int i= directoryStart; i< directoryStart+directorySize; i++){
        buffer.write(fat16.getDisk().readSector(i));
    }
    final byte[] dirBinary = buffer.toByteArray();
    DirectoryEntry [] directoryEntry = new DirectoryEntry[fat16.getNumberOfDirEntriesPerCluster()*directorySize];
    for (int i= 0; i< directoryEntry.length; i++){
        byte[] binaryEntry = Arrays.copyOfRange(dirBinary, i*DirectoryEntry.SIZE_BYTES, i*DirectoryEntry.SIZE_BYTES + DirectoryEntry.SIZE_BYTES);
        StringBuffer buf = new StringBuffer();
        for(byte b:binaryEntry){
            buf.append(Integer.toHexString(Byte.toUnsignedInt(b))+" ");
        }
        logger.debug("binary Entry: "+buf.toString());
        directoryEntry[i] = Codecs.decode(directoryEntryCodec, binaryEntry); //FIXME here just the filename is set correctly
        logger.debug("bad result with only the first field set the rest still on standard values:");
        logger.debug(directoryEntry[i]);
    }

    return directoryEntry;
}

And an image of my test output:

output

My guess was that the encoding parameter in the first field is a problem, but even without it, the situation remains unchanged.
I kindly ask for help.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions