Binärdateien auslesen mit dem ByteBuffer

Es hat zugegebener Maßen eine Weile gedauert, bis ich die Klasse ByteBuffer entdeckt habe, aber dafür will ich sie mittlerweile nicht mehr missen, wenn ich Datendateien auswerden muss darf. Insbesondere bei binären Datenformaten ist diese Klasse aus dem Paket java.nio eine unverzichtbare Hilfe.

Im Moment versuche ich zum Beispiel Ressourcen aus einer Windows exe Datei zu extrahieren. Da ich keine wirkliche Java Implementierung dafür gefunden habe, habe ich mich selber dran gesetzt und mich durch den Dschungel auch Header, Directories und Directory Einträgen gekämpft. In einer solchen “PE Dateien” wimmelt es nur so auch Datenstukturen die ihren Ursprung in einem C struct haben. Diese lassen sich halt von der Windows API ziemlich einfach einlesen (Stichwort “Memory Dump”). Für Java bedeutet dies aber in der Regel einen Mehraufwand. Die beginnt schon bei der Tatsache, dass Java Binärdaten als BIG_ENDIAN speichert, aber unter Windows LITTLE_ENDIAN eigentlich der Standard ist.

Aber genau hier setzt der ByteBuffer an. Man könnte dazu auch fast sagen, ein ByteBuffer ist ein gepimptes byte Array. Ausgestattet mit diversen Settern und Gettern um atomare Java Daten hintereinander abzulegen und auszulesen bietet es unter anderem auch an die Bytereihenfolge (“ByteOrder“) einzustellen. Wir müssen nur eine Bytefolge (z.B. aus einem InputStream) in den Puffer schreiben und danach können wir direkt short, int oder long Werte auslesen. Passend aufbereitet für unser Java Programm.

Damit ich auch in Java einfach auf eine C struct Ähnliche Datenstruktur zugreifen kann, habe ich mir eine kleine Basisklasse geschrieben:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
package de.nigjo.blog.netbeans;

import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.text.MessageFormat;

/**
 * "C"-Struct.
 *
 * {@code long} = DWORD = 4 byte,
 * {@code int} = WORD = 2 byte,
 * {@code short} = BYTE = 1 byte
 *
 * <p>
 * http://blog.nigjo.de/netbeans/2010/08/cstruct/
 * @author nigjo
 */

public abstract class Struct
{
  private static final long DWORD_MASK = 0xFFFFFFFFl;
  private static final int WORD_MASK = 0xFFFF;
  private static final short BYTE_MASK = 0xFF;

  private int getSize()
  {
    Size size = getClass().getAnnotation(Size.class);
    if(size == null)
      throw new IllegalStateException(
          "missing @Size for " + getClass().getName());
    return size.value();
  }

  @Retention(RetentionPolicy.RUNTIME)
  public @interface Size
  {
    int value();
  }

  @Retention(RetentionPolicy.RUNTIME)
  protected @interface NoStructElement
  {
  }

  public void read(InputStream s) throws IOException
  {
    ByteBuffer buffer = ByteBuffer.allocate(getSize());
    buffer.order(ByteOrder.LITTLE_ENDIAN);
    s.read(buffer.array());
    read(buffer);
  }

  public void read(ByteBuffer parent, int offset)
  {
    byte[] cache = new byte[getSize()];
    parent.position(offset);
    parent.get(cache);
    ByteBuffer buffer = ByteBuffer.wrap(cache);
    buffer.order(parent.order());
    read(buffer);
  }

  public void read(ByteBuffer buffer)
  {
    if(buffer.capacity() != getSize())
    {
      throw new IllegalArgumentException(
          MessageFormat.format("different buffer size {0}!={1}",
          buffer.capacity(), getSize()));
    }
    try
    {
      Field[] fields = getClass().getDeclaredFields();
      for(Field field : fields)
      {
        if((field.getModifiers() & Modifier.STATIC) != 0)
          continue;
        if(field.getAnnotation(NoStructElement.class) != null)
          continue;
        Class<?> type = field.getType();
        if(type.equals(long.class))
        {
          long value = buffer.getInt() & DWORD_MASK;
          field.setLong(this, value);
        }
        else if(type.equals(int.class))
        {
          int value = buffer.getShort() & WORD_MASK;
          field.setInt(this, value);
        }
        else if(type.equals(short.class))
        {
          short value = (short)(buffer.get() & BYTE_MASK);
          field.setShort(this, value);
        }
        else
        {
          Size size = field.getAnnotation(Size.class);
          if(size == null)
          {
            throw new IllegalStateException(
                "missing @Size for " + field.getName());
          }
          if(type.equals(byte[].class))
          {
            byte[] reserved = new byte[size.value()];
            buffer.get(reserved);
            field.set(this, reserved);
          }
          else if(type.equals(Struct.class))
          {
            Struct struct = (Struct)field.get(this);
            if(struct == null)
            {
              struct = type.asSubclass(Struct.class).newInstance();
              field.set(this, struct);
            }
            struct.read(buffer, buffer.position());
          }
          else
          {
            throw new IllegalStateException(
                "unknown type " + type.getName());
          }
        }
      }
      if(buffer.hasRemaining())
        throw new IllegalArgumentException(
            "size missmatch " + buffer.position() + "!=" + buffer.limit());
    }
    catch(InstantiationException iae)
    {
      throw new IllegalStateException(iae);
    }
    catch(IllegalAccessException iae)
    {
      throw new IllegalStateException(iae);
    }
  }
}

Mit Hilfe dieser Klasse lassen sich jetzt die Header für eine PE Datei relativ einfach erstellen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * Common Object File Format (COFF).
 * http://support.microsoft.com/kb/121460
 */

@Struct.Size(20)
public class CoffHeader extends Struct
{
  int machine;
  int numberOfSections;
  long timeStamp;
  long offsetSymbolTable;
  long numberOfSymbols;
  int optionalHeaderSize;
  int characteristics;
}

Ein Aufruf von

1
coffHeader.read(inputStream);

reicht um die einzelnen Klassenvariablen mit den korrekten Werten zu besetzten.

Beschreibung der Klasse

Wie in dem Beispiel “CoffHeader” zu sehen sind diese “Struct” Implementierungen nicht unbedingt mit dem Grundsatz des “Datenkapselung” vereinbar. Das finde ich in diesem Fall aber nicht so schlimm. Diese Datenstruktur ist für “normale” Anwendungen einfach zu “unhandlich” und wird vorrangig nur zur Daten Ein-/Ausgabe verwendet. Darum habe ich die Datenkapselung der “Einfachheit” geopfert.

Wie im JavaDoc der Struct Klasse zu sehen ist, kann ich drei ganzzahlige Typen aus der Binärdatei direkt auswerten. 1, 2 oder 4 Byte Ganzzahlen. Dies ist für die meisten Dateiheader ausreichend. Andere Strukturen müssten dann noch einmal gesondert behandelt werden. Um aber auch diese Daten aus dem Header lesen zu können kann man auch ein byte[] Array in seinem Header deklarieren und mit der @Size Annotation die Große festlegen. In diesem Parameter wird dann die Daten direkt aus der Daten Byte-weise abgelegt. Diese Möglichkeit ist auch für diverse “reserved” Werte, die in Headern gerne mal vorkommen interessant.

Damit eine Struct Implementierung auch noch weitere Klassenvariablen enthalten kann, gibt es noch die Annotation @NoStructElement. Alle Variablen, die auf diese Weise markiert wurden, werden von der Auswerteroutine read(ByteBuffer) ignoriert.

Jens

Sie können eine Kommentar,oderTrackback von Ihrer Webseite hinterlassen.

5 Responses to “Binärdateien auslesen mit dem ByteBuffer”

  1. Die Klasse funktioniert für mich. Sollte jemand Fehler finden oder Anmerkungen haben, bitte kommentieren.

    Jens

  2. Unterstützung für Strings

    Ich habe noch eine Unterstützung für Strings eingefügt (solange sie nicht eine dynamische Länge haben). Dazu habe ich zum einen der Annotation @Size die Zeile

    1
    public String encoding() default "";

    hinzugefügt (Zeile 43) und in der Methode read(ByteBuffer) vor der Zeile 115

    1
    else if(type.equals(Struct.class))

    die folgenden Zeilen eingefügt:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    else if(type.equals(String.class))
    {
      String encoding = size.encoding();
      if(encoding.equals(""))
      {
        throw new IllegalStateException(
            "missing encoding for " + field.getName());
      }
      byte[] data = new byte[size.value()];
      buffer.get(data);
      try
      {
        String text = new String(data, encoding);
        if(text.indexOf('\0') >= 0)
          text = text.substring(0, text.indexOf('\0'));
        field.set(this, text);
      }
      catch(UnsupportedEncodingException ex)
      {
        throw new IllegalStateException(ex);
      }
    }

    Damit konnte ich in die Headerstrukturen auch solche Zeilen eintragen:

    1
    2
      @Size(value=8, encoding="UTF-8")
      public String name;
  3. Marcus sagt:

    Hallo Jens,

    erstmal vielen Dank, ich suche bereits seit Längerem nach einer solchen Implemtierung für Java. Aufgrund meiner generellen Unerfahrenheit im Umgang mit Java benötige ich deine Hilfe bzw. habe ich eine Frage. Für ein Projekt muss ich den Wert, welchen man durch (Windows XP) –> –> “Elementname” –> Produktversion = Wert, dem Header entnehmen kann, auslesen können. Meine Frage: Kann ich diesen Wert mit deiner Implemtierung auslesen? Wenn ja kannst du mir hierbei auf die Sprünge helfen, was die Umsetzung betrifft?

    Vielen Dank
    Marcus

  4. Hi Markus
    Ich weiß jetzt nicht genau, was du mit

    (Windows XP) –> –> “Elementname” –> Produktversion = Wert


    meinst.

    Grundsätzlich kannst du die Struct Klasse für beliebige Binärdaten nutzen. Der COFF Header ist nur ein Beispiel und die genauen Inhalte sind auf den MSDN Seiten viel besser beschrieben als ich das hier in den Kommentaren schreiben könnte. Ich müsste mich in die PE Strukturen selber erst wieder einlesen. Sorry.

    schönen Gruß
    Jens

  5. Marcus sagt:

    Hallo Jens,

    Sorry, meine Beschreibung war schlecht. Kurz in Windows(XP) kann meine eine .dll oder .exe mit Rechtermaustaste und Eigenschaften etc. pp. unteranderem die Produktversion auslesen.
    Bin jetzt auf der msdn-Seite gelandet dort steht, so verstehe ich es, wie man über die version.dll und deren Funktionen an die Werte kommen kann. Dafür muss in Java wieder was mit JNA gemacht werden…

    Dennoch Danke.
    Marcus

Leave a Reply


9 + = achtzehn