~/home of geeks

File Types

· 1150 Wörter · 6 Minute(n) Lesedauer

Und wo wir doch mal gerade dabei sind, alles selber zu machen: In der JDK fehlt eine komfortable Art, den Dateityp einer Datei zu bestimmen. Zwar gibt es im Packet activation Möglichkeiten, über eine DataSource den MimeTyp einer Datei zu ermitteln, aber sehr komfortabel ist dies nicht. Und es wird wahrscheinlich nur die Dateinamenerweiterung benutzt. Ich werde hier mal erörtern, wie ich das samt Fileheader-Auswertung gelöst habe.

Diesem Problem ist auch Marco Schmidt begegnet und hat eine kleine API namens “ffident” geschrieben, welche sogar die Fileheader auswerten kann.

Fileheader sind meistens die ersten paar Bytes einer Datei, die für fast jeden Dateityp fix sind (ausgenommen Textdateien).

Doch auch diese Lösung erschien mir nicht passend genug, also habe ich kurzerhand eine eigene Version geschrieben.

Nicht alle Dateiformate besitzen einen eigenen Header, und wenn doch, kann es dennoch vorkommen, dass mehrere Formate denselben Header haben, wie z. B. Morgokruf Office Dokumente, wo Wort und Eksel Dokumente dieselben Header besitzen.

Die von mir geschriebenen Klassen lesen ihre Informationen aus einer Properties-Datei und werten beim Bestimmen der Dateitypen wahlweise den Dateiheader, die Dateinamenerweiterung oder beides aus. Zusätzlich werden einige Meta-Informationen über den Dateityp abgelegt, wie z. B. eine Beschreibung und ob es sich um ein Archiv handelt.

mimetypes.properties

mimetype.0=application/zip
mimetype.0.fileext=zip, jar
mimetype.0.browser=application/zip
mimetype.0.description=ZIP Archive
mimetype.0.magicbytes=50 4B 03 04
mimetype.0.isarchive=true

mimetype.1=application/pdf
mimetype.1.fileext=pdf
mimetype.1.browser=application/pdf
mimetype.1.description=Acrobat PDF Document
mimetype.1.magicbytes=25 50 44 46 2D
mimetype.1.isarchive=false

[...]

mimetype.16=application/java-byte-code
mimetype.16.fileext=class
mimetype.16.browser=application/java-byte-code
mimetype.16.description=Java Bytecode
mimetype.16.magicbytes=CA FE BA BE 00
mimetype.16.isarchive=false

Dank meiner ExtendedProperties steht bei fileext eine Liste von Dateiendungen, die typisch für das jeweilige Format sind. In Magicbytes steht der Fileheader in Hex-kodierter Form. So kann die Erkennung auf beliebige Dateitypen erweitert werden (Bilder, Audio, Video etc.). Da ich derzeit an einer Anwendung für Texte arbeite, enthält meine Datei lediglich weiter verbreitete Textformate und Archive.

Die entsprechende MimeType Klasse sieht dann so aus:

package librarian.util.mimetype;

import java.util.HashSet;
import java.util.Set;

import librarian.util.StringUtils;

public class MimeType{
private String mimeType;
private HashSet extensions = new HashSet();
private String description;
private String browserContentType;
private String magicBytes; // as hexstring
private boolean isArchive;

public String getBrowserContentType() {
  return browserContentType;
}

public void setBrowserContentType(String 
browserContentType) {
  this.browserContentType = browserContentType;
}

public String getDescription() {
  return description;
}

public void setDescription(String description) {
  this.description = description;
}

public Set getExtensions() {
  return extensions;
}

public void addExtension(String extension){
  if (!StringUtils.isEmptyWithTrim(extension)){
    extensions.add(extension.trim());
  }
}

public String getMagicBytes() {
  return magicBytes;
}

public void setMagicBytes(String magicBytes) {
  this.magicBytes = magicBytes;
}

public String getMimeType() {
  return mimeType;
}

public void setMimeType(String mimeType) {
  this.mimeType = mimeType;
}

public boolean isArchive(){
  return isArchive;
}

public void setIsArchive(boolean isArchive){
  this.isArchive = isArchive;
}

public String toString(){
  return "mime="+mimeType+", 
    browser="+browserContentType+
    ", magic="+magicBytes+
    ", ext="+extensions+
    ", archiv="+Boolean.toString(isArchive)+
    ", desc="+description;
  }
}

Die Magicbytes werden von Leerzeichen befreit und als String in einer Map abgelegt. Sobald eine Datei geprüft werden soll, wird sie byteweise ausgelesen (unter zur Hilfenahme eines BufferedInputStreams), in einen Hexstring umgewandelt und anschließend mit der Map verglichen. So kann die Erkennung recht schnell arbeiten und greift nur einmal auf die Datei zu.

package librarian.util.mimetype;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import librarian.util.StringUtils;
import librarian.util.file.FileUtil;
import librarian.util.properties.ExtendedProperties;

import org.apache.log4j.Logger;

public class MimeTypeDetector {
    private static final Logger LOG = Logger.getLogger(MimeTypeDetector.class);

    private HashMap fileExtensionsToMimeTypes =
        new HashMap(); // sets
    private HashMap magicBytesToMimeTypes =
        new HashMap();
    private HashMap mimeTypesToPropertyIds =
        new HashMap();
    private int maxMagicByteLen; // längster header

    public MimeTypeDetector(ExtendedProperties config) {
        int number = 0;
        List tokens;
        String token;
        boolean bool;
        while (true) {
            token = config.getString("mimetype." + number);
            if (StringUtils.isEmptyWithTrim(token))
                break;
            else {
                if (LOG.isInfoEnabled())
                    LOG.info("registering mimetype " + token);
                MimeType mimetype = new MimeType();
                mimetype.setMimeType(token.trim().toLowerCase());
                tokens = config.getList("mimetype." + number + ".fileext", true);
                if (tokens.size() > 0) {
                    for (int i = 0; i < tokens.size(); i++) {
                        token = (String) tokens.get(i);
                        if (!StringUtils.isEmptyWithTrim(token))
                            mimetype.addExtension(token.trim().toLowerCase());
                    }
                } else {
                    LOG.warn("mimetype \\"" + mimetype.getMimeType() +
                        "\\" is missing file extensions. it will be skipped.");
                    number++;
                    continue;
                }

                token = config.getString("mimetype." + number + ".browser");
                if (StringUtils.isEmptyWithTrim(token))
                    token = mimetype.getMimeType();
                mimetype.setBrowserContentType(
                    token.trim().toLowerCase());

                token = config.getString("mimetype." + number +
                    ".description");
                if (StringUtils.isEmptyWithTrim(token))
                    token = mimetype.getMimeType();
                mimetype.setDescription(token.trim());

                bool = config.getBoolean("mimetype." + number +
                    ".isarchive");
                mimetype.setIsArchive(bool);


                token = config.getString("mimetype." + number +
                    ".magicbytes");
                if (!StringUtils.isEmptyWithTrim(token)) {
                    mimetype.setMagicBytes(
                        token.trim().toUpperCase().
                            replaceAll("\\\\s", ""));
                    magicBytesToMimeTypes.put(
                        mimetype.getMagicBytes(), mimetype);
                    if (maxMagicByteLen < token.length())
                        maxMagicByteLen = token.length();
                }

                Set mfileext = mimetype.getExtensions();
                Iterator iter = mfileext.iterator();
                while (iter.hasNext()) {
                    String ext = (String) iter.next();
                    Set fileext = (Set) fileExtensionsToMimeTypes.
                        get(ext);
                    if (fileext == null) {
                        fileext = new HashSet();
                        fileExtensionsToMimeTypes.put(
                            ext, fileext);
                    }
                    fileext.add(mimetype);
                }

                mimeTypesToPropertyIds.put(
                    new Integer(number), mimetype);
                number++;
            }
        }
    }

    /**
     * Try to detect by extension.
     *
     * @param f
     * @return
     */
    public MimeType[] detectByFileExtension(File f) {
        String extension = FileUtil.getFileExtension(
            f.getName());
        if (StringUtils.isEmptyWithTrim(extension))
            return null;
        Set mimes = (Set) this.fileExtensionsToMimeTypes.
            get(extension);
        if (mimes == null || mimes.size() <= 0)
            return null;
        return (MimeType[]) mimes.toArray(
            new MimeType[mimes.size()]);
    }

    /**
     * Try to detect by fileheader.
     *
     * @param f
     * @return
     */
    public MimeType[] detectByFileHeader(File f) {
        LinkedHashSet mimes = new LinkedHashSet();
        InputStream fi = null;
        MimeType mime = null;
        try {
            fi = new BufferedInputStream(
                new FileInputStream(f),
                maxMagicByteLen + 10);
            int readBytes = 0;
            int readByte = 0;
            StringBuffer magicBytes =
                new StringBuffer(maxMagicByteLen + 10);
            String magic;
            String hex;
            while (readBytes <= maxMagicByteLen &&
                readByte > -1) {

                readByte = fi.read();
                readBytes++;
                hex = Integer.toHexString(readByte);
                if (hex.length() < 2) hex = "0" + hex;
                magicBytes.append(hex);
                magic = magicBytes.toString().
                    toUpperCase();
                if (LOG.isDebugEnabled())
                    LOG.debug("magic: " + magic);
                mime = (MimeType) magicBytesToMimeTypes.
                    get(magic);
                if (mime != null) mimes.add(mime);
            }

            if (mimes.size() <= 0) return null;
            return (MimeType[]) mimes.
                toArray(new MimeType[mimes.size()]);
        } catch (IOException e) {
            LOG.warn(e);
            return null;
        } finally {
            try {
                if (fi != null) fi.close();
            } catch (Throwable t) {
            }
        }
    }

    /**
     * First try to detect filetype by file header,
     * then by fileextension.
     *
     * @param f
     * @return
     */
    public MimeType[] detect(File f) {
        MimeType[] mimes = detectByFileHeader(f);
        if (mimes == null) mimes = detectByFileExtension(f);
        return mimes;
    }
}

Eine Beispielausgabe für eine ZIP und eine PDF-Datei sieht wie folgt aus (inkl. Logging):

10:29:08 INFO  MimeTypeDetector.<init>: registering mimetype application/zip
10:29:08 INFO  MimeTypeDetector.<init>: registering mimetype application/pdf
10:29:08 INFO  MimeTypeDetector.<init>: registering mimetype application/gzip
10:29:08 INFO  MimeTypeDetector.<init>: registering mimetype application/postscript
10:29:08 INFO  MimeTypeDetector.<init>: registering mimetype text/plain
10:29:08 INFO  MimeTypeDetector.<init>: registering mimetype application/msword
10:29:08 INFO  MimeTypeDetector.<init>: registering mimetype application/rtf 
10:29:08 INFO  MimeTypeDetector.<init>: registering mimetype text/xml
10:29:08 INFO  MimeTypeDetector.<init>: registering mimetype text/html
10:29:08 INFO  MimeTypeDetector.<init>: registering mimetype application/mspowerpoint
10:29:08 INFO  MimeTypeDetector.<init>: registering mimetype application/x-tex
10:29:08 INFO  MimeTypeDetector.<init>: registering mimetype application/tar
10:29:14 DEBUG MimeTypeDetector.detect: magic: 50
10:29:14 DEBUG MimeTypeDetector.detect: magic: 504B
10:29:14 DEBUG MimeTypeDetector.detect: magic: 504B03
10:29:14 DEBUG MimeTypeDetector.detect: magic: 504B0304
mime=application/zip, browser=application/zip, magic=504B0304, ext=[zip, jar], archiv=true, desc=ZIP Archive
10:29:15 DEBUG MimeTypeDetector.detect: magic: 25
10:29:15 DEBUG MimeTypeDetector.detect: magic: 2550
10:29:15 DEBUG MimeTypeDetector.detect: magic: 255044
10:29:15 DEBUG MimeTypeDetector.detect: magic: 25504446
10:29:15 DEBUG MimeTypeDetector.detect: magic: 255044462D
mime=application/pdf, browser=application/pdf, magic=255044462D, ext=[pdf], archiv=false, desc=Acrobat PDF Document