Dépôt officiel du code source de l'ERP OpenConcerto
Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
*
* The contents of this file are subject to the terms of the GNU General Public License Version 3
* only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
* copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each file.
*/
package org.openconcerto.utils.mime;
/*
* Copyright 2007-2009 Medsea Business Solutions S.L.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.Log;
import org.openconcerto.utils.OSFamily;
import org.openconcerto.utils.StreamUtils;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.spi.FileTypeDetector;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.logging.Level;
import java.util.regex.Pattern;
/**
* <p>
* The Opendesktop shared mime database contains glob rules and magic number lookup information to
* enable applications to detect the mime types of files.
* </p>
* <p>
* This class uses the mime.cache file which is one of the files created by the update-mime-database
* application. This file is a memory mapped file that enables the database to be updated and copied
* without interrupting applications.
* </p>
* <p>
* This implementation follows the memory mapped spec so it is not required to restart an
* application using this mime detector should the underlying mime.cache database change.
* </p>
* <p>
* For a complete description of the information contained in this file please see:
* http://standards.freedesktop.org/shared-mime-info-spec/shared-mime-info-spec-latest.html
* </p>
* <p>
* This class also follows, where possible, the RECOMENDED order of detection as detailed in this
* spec. Thanks go to Mathias Clasen at Red Hat for pointing me to the original xdgmime
* implementation http://svn.gnome.org/viewvc/glib/trunk/
* gio/xdgmime/xdgmimecache.c?revision=7784&view=markup
* </p>
* More up to date : https://github.com/GNOME/beagle/blob/master/beagle/glue/xdgmime/xdgmimecache.c
*
* @author Steven McArdle
* @see <a
* href="http://standards.freedesktop.org/shared-mime-info-spec/shared-mime-info-spec-latest.html">Shared
* MIME-Info</a>
*/
@SuppressWarnings({ "unqualified-field-access" })
public class FreeDesktopMimeDetector extends FileTypeDetector {
public static enum Mode {
/**
* <a href=
* "http://standards.freedesktop.org/shared-mime-info-spec/shared-mime-info-spec-latest.html#idm140625828606432"
* >Recommended checking order</a>
*/
RECOMMENDED, DATA_ONLY, NAME_ONLY
}
public static final String DEFAULT_CACHE = "freeDesktop.mime.cache";
public static final String DEFAULT_MODE = "freeDesktop.mime.mode";
static private Mode getDefaultMode() {
final String m = System.getProperty(DEFAULT_MODE);
if (m != null) {
try {
return Mode.valueOf(m.toUpperCase());
} catch (Exception e) {
Log.get().log(Level.CONFIG, "Ignoring invalid mode : " + m, e);
}
}
return Mode.RECOMMENDED;
}
private static File mimeCacheFile = OSFamily.getInstance() == OSFamily.Windows ? null : new File("/usr/share/mime/mime.cache");
static private boolean canReadFile(final File f, final String msg) {
if (f == null)
return false;
final boolean res = f.isFile() && f.canRead();
if (!res)
Log.get().config(msg + f);
return res;
}
static private Object getDefaultCache() {
final String m = System.getProperty(DEFAULT_CACHE);
final File f = m != null ? new File(m) : null;
if (canReadFile(f, "Ignoring invalid passed file : ")) {
return f;
} else if (canReadFile(mimeCacheFile, "Ignoring invalid system file : ")) {
return mimeCacheFile;
} else {
final URL res = FreeDesktopMimeDetector.class.getResource("mime.cache");
if (res == null)
throw new IllegalStateException("No mime.cache found for " + FreeDesktopMimeDetector.class);
return res;
}
}
private final ByteBuffer content;
private final Mode mode;
public FreeDesktopMimeDetector() throws IOException {
this(getDefaultCache(), getDefaultMode());
}
public FreeDesktopMimeDetector(final File mimeCacheFile, final Mode mode) throws IOException {
this((Object) mimeCacheFile, mode);
}
public FreeDesktopMimeDetector(final InputStream is, final Mode mode) throws IOException {
this((Object) is, mode);
}
// InputStream will be closed
private FreeDesktopMimeDetector(final Object cache, final Mode mode) throws IOException {
if (cache instanceof File || cache instanceof FileInputStream) {
// Map the mime.cache file as a memory mapped file
try (final FileInputStream is = cache instanceof FileInputStream ? (FileInputStream) cache : new FileInputStream((File) cache); final FileChannel rCh = is.getChannel();) {
content = rCh.map(FileChannel.MapMode.READ_ONLY, 0, rCh.size());
}
} else {
final ByteArrayOutputStream out = new ByteArrayOutputStream(250 * 1024);
try (final InputStream is = cache instanceof URL ? ((URL) cache).openStream() : (InputStream) cache) {
StreamUtils.copy(is, out);
}
content = ByteBuffer.wrap(out.toByteArray());
}
this.mode = mode;
}
@Override
public String probeContentType(Path path) throws IOException {
final Collection<String> col;
switch (this.mode) {
case RECOMMENDED:
col = this.getMimeTypesFile(path.toFile());
break;
case DATA_ONLY:
try (final InputStream is = new BufferedInputStream(new FileInputStream(path.toFile()))) {
col = this.getMimeTypesInputStream(is);
}
break;
case NAME_ONLY:
col = this.getMimeTypesFileName(path.getFileName().toString());
break;
default:
throw new IllegalStateException("Unknown mode : " + this.mode);
}
return CollectionUtils.getFirst(col);
}
/**
* This method resolves mime types closely in accordance with the RECOMENDED order of detection
* detailed in the Opendesktop shared mime database specification
* http://standards.freedesktop.org/shared-mime-info-spec/shared-mime-info-spec-latest.html See
* the Recommended checking order.
*
* @param fileName the name to inspect.
* @return a collection of MIME types.
*/
public Collection<String> getMimeTypesFileName(String fileName) {
Collection<WeightedMimeType> mimeTypes = new ArrayList<WeightedMimeType>();
// Lookup the globbing methods first
lookupMimeTypesForGlobFileName(fileName, mimeTypes);
return normalizeWeightedMimeList(mimeTypes);
}
/**
* This method resolves mime types closely in accordance with the RECOMENDED order of detection
* detailed in the Opendesktop shared mime database specification
* http://standards.freedesktop.org/shared-mime-info-spec/shared-mime-info-spec-latest.html See
* the Recommended checking order.
*
* @param file the file to inspect.
* @return a collection of MIME types.
* @throws IOException if the file couldn't be read.
*/
public Collection<String> getMimeTypesFile(File file) throws IOException {
Collection<String> mimeTypes = getMimeTypesFileName(file.getName());
if (!file.exists()) {
return mimeTypes;
}
try (final InputStream is = new BufferedInputStream(new FileInputStream(file))) {
return _getMimeTypes(mimeTypes, is);
}
}
/**
* This method is unable to perform glob matching as no name is available. This means that it
* does not follow the recommended order of detection defined in the shared mime database spec
* http://standards.freedesktop.org/shared-mime-info-spec/shared-mime-info-spec-latest.html
*
* @param in the stream to inspect.s
* @return a collection of MIME types.
* @throws IOException if the stream couldn't be read.
*/
public Collection<String> getMimeTypesInputStream(InputStream in) throws IOException {
return lookupMimeTypesForMagicData(in);
}
/**
* This method is unable to perform glob matching as no name is available. This means that it
* does not follow the recommended order of detection defined in the shared mime database spec
* http://standards.freedesktop.org/shared-mime-info-spec/shared-mime-info-spec-latest.html
*
* @param data the data to inspect.
* @return a collection of MIME types.
*/
public Collection<String> getMimeTypesByteArray(byte[] data) {
return lookupMagicData(data);
}
@Override
public String toString() {
return this.getClass().getSimpleName() + " using the mime.cache file version [" + getMajorVersion() + "." + getMinorVersion() + "].";
}
public String dump() {
return "{MAJOR_VERSION=" + getMajorVersion() + " MINOR_VERSION=" + getMinorVersion() + " ALIAS_LIST_OFFSET=" + getAliasListOffset() + " PARENT_LIST_OFFSET=" + getParentListOffset()
+ " LITERAL_LIST_OFFSET=" + getLiteralListOffset() + " REVERSE_SUFFIX_TREE_OFFSET=" + getReverseSuffixTreeOffset() + " GLOB_LIST_OFFSET=" + getGlobListOffset() + " MAGIC_LIST_OFFSET="
+ getMagicListOffset() + " NAMESPACE_LIST_OFFSET=" + getNameSpaceListOffset() + " ICONS_LIST_OFFSET=" + getIconListOffset() + " GENERIC_ICONS_LIST_OFFSET="
+ getGenericIconListOffset() + "}";
}
private Collection<String> lookupMimeTypesForMagicData(InputStream in) throws IOException {
int offset = 0;
int len = getMaxExtents();
byte[] data = new byte[len];
// Mark the input stream
in.mark(len);
try {
// Since an InputStream might return only some data (not all
// requested), we have to read in a loop until
// either EOF is reached or the desired number of bytes have been
// read.
int restBytesToRead = len;
while (restBytesToRead > 0) {
int bytesRead = in.read(data, offset, restBytesToRead);
if (bytesRead < 0)
break; // EOF
offset += bytesRead;
restBytesToRead -= bytesRead;
}
} finally {
// Reset the input stream to where it was marked.
in.reset();
}
return lookupMagicData(data);
}
private Collection<String> lookupMagicData(byte[] data) {
Collection<String> mimeTypes = new ArrayList<String>();
int listOffset = getMagicListOffset();
int numEntries = content.getInt(listOffset);
int offset = content.getInt(listOffset + 8);
for (int i = 0; i < numEntries; i++) {
final int matchOffset = offset + (16 * i);
final String mimeType = compareToMagicData(matchOffset, data);
if (mimeType != null) {
mimeTypes.add(mimeType);
} else {
final String nonMatch = getMimeType(content.getInt(matchOffset + 4));
mimeTypes.remove(nonMatch);
}
}
return mimeTypes;
}
private String compareToMagicData(int offset, byte[] data) {
// TODO
// int priority = content.getInt(offset);
int mimeOffset = content.getInt(offset + 4);
int numMatches = content.getInt(offset + 8);
int matchletOffset = content.getInt(offset + 12);
for (int i = 0; i < numMatches; i++) {
if (matchletMagicCompare(matchletOffset + (i * 32), data)) {
return getMimeType(mimeOffset);
}
}
return null;
}
private boolean matchletMagicCompare(int offset, byte[] data) {
int n_children = content.getInt(offset + 24);
int child_offset = content.getInt(offset + 28);
if (magic_matchlet_compare_to_data(offset, data)) {
if (n_children == 0)
return true;
for (int i = 0; i < n_children; i++) {
if (matchletMagicCompare(child_offset + 32 * i, data))
return true;
}
}
return false;
}
private boolean magic_matchlet_compare_to_data(int offset, byte[] data) {
int rangeStart = content.getInt(offset);
int rangeLength = content.getInt(offset + 4);
int dataLength = content.getInt(offset + 12);
int dataOffset = content.getInt(offset + 16);
int maskOffset = content.getInt(offset + 20);
for (int i = rangeStart; i <= rangeStart + rangeLength; i++) {
boolean validMatch = true;
if (i + dataLength > data.length) {
return false;
}
if (maskOffset != 0) {
for (int j = 0; j < dataLength; j++) {
if ((content.get(dataOffset + j) & content.get(maskOffset + j)) != (data[j + i] & content.get(maskOffset + j))) {
validMatch = false;
break;
}
}
} else {
for (int j = 0; j < dataLength; j++) {
if (content.get(dataOffset + j) != data[j + i]) {
validMatch = false;
break;
}
}
}
if (validMatch) {
return true;
}
}
return false;
}
private void lookupGlobLiteral(String fileName, Collection<WeightedMimeType> mimeTypes) {
int listOffset = getLiteralListOffset();
int numEntries = content.getInt(listOffset);
int min = 0;
int max = numEntries - 1;
while (max >= min) {
int mid = (min + max) / 2;
String literal = getString(content.getInt((listOffset + 4) + (12 * mid)));
int cmp = literal.compareTo(fileName);
if (cmp < 0) {
min = mid + 1;
} else if (cmp > 0) {
max = mid - 1;
} else {
String mimeType = getMimeType(content.getInt((listOffset + 4) + (12 * mid) + 4));
int weight = content.getInt((listOffset + 4) + (12 * mid) + 8);
mimeTypes.add(new WeightedMimeType(mimeType, literal, weight));
return;
}
}
}
private void lookupGlobFileNameMatch(String fileName, Collection<WeightedMimeType> mimeTypes) {
final int listOffset = getGlobListOffset();
final int numEntries = content.getInt(listOffset);
final int entriesOffset = listOffset + 4;
for (int i = 0; i < numEntries; i++) {
final int entryOffset = entriesOffset + 12 * 1;
final int offset = content.getInt(entryOffset);
final int mimeTypeOffset = content.getInt(entryOffset + 4);
final int weightNFlags = content.getInt(entryOffset + 8);
final int weight = weightNFlags & 0xFF;
final boolean cs = (weightNFlags & 0x100) != 0;
final Pattern pattern = Pattern.compile(getString(offset, true), !cs ? (Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE) : 0);
if (pattern.matcher(fileName).matches()) {
final String mimeType = getMimeType(mimeTypeOffset);
final String globPattern = getString(offset, false);
mimeTypes.add(new WeightedMimeType(mimeType, globPattern, weight));
}
}
}
private Collection<String> normalizeWeightedMimeList(Collection<WeightedMimeType> weightedMimeTypes) {
if (weightedMimeTypes.isEmpty())
return Collections.emptySet();
Collection<WeightedMimeType> mimeTypes = new LinkedHashSet<WeightedMimeType>();
// Sort the weightedMimeTypes
Collections.sort((List<WeightedMimeType>) weightedMimeTypes, new Comparator<WeightedMimeType>() {
public int compare(WeightedMimeType obj1, WeightedMimeType obj2) {
return obj1.weight - obj2.weight;
}
});
// Keep only globs with the biggest weight. They are in weight order at
// this point
int weight = 0;
int patternLen = 0;
for (final WeightedMimeType mw : weightedMimeTypes) {
if (weight < mw.weight) {
weight = mw.weight;
}
if (weight >= mw.weight) {
if (mw.pattern.length() > patternLen) {
patternLen = mw.pattern.length();
}
mimeTypes.add(mw);
}
}
// Now keep only the longest patterns
for (final WeightedMimeType mw : weightedMimeTypes) {
if (mw.pattern.length() < patternLen) {
mimeTypes.remove(mw);
}
}
// Could possibly have multiple mimeTypes here with the same weight and
// pattern length. Can even have multiple entries for the same type so
// lets remove
// any duplicates by copying entries to a HashSet that can only have a
// single instance
// of each type
Collection<String> _mimeTypes = new HashSet<String>();
for (final WeightedMimeType mw : mimeTypes) {
_mimeTypes.add(mw.toString());
}
return _mimeTypes;
}
private void lookupMimeTypesForGlobFileName(String fileName, Collection<WeightedMimeType> mimeTypes) {
if (fileName == null) {
return;
}
lookupGlobLiteral(fileName, mimeTypes);
if (!mimeTypes.isEmpty()) {
return;
}
int len = fileName.length();
lookupGlobSuffix(fileName, false, len, mimeTypes);
if (mimeTypes.isEmpty()) {
lookupGlobSuffix(fileName, true, len, mimeTypes);
}
if (mimeTypes.isEmpty()) {
lookupGlobFileNameMatch(fileName, mimeTypes);
}
}
private void lookupGlobSuffix(String fileName, boolean ignoreCase, int len, Collection<WeightedMimeType> mimeTypes) {
int listOffset = getReverseSuffixTreeOffset();
int numEntries = content.getInt(listOffset);
int offset = content.getInt(listOffset + 4);
lookupGlobNodeSuffix(fileName, numEntries, offset, ignoreCase, len, mimeTypes, new StringBuffer());
}
private void lookupGlobNodeSuffix(final String fileName, final int numEntries, final int offset, final boolean ignoreCase, int len, Collection<WeightedMimeType> mimeTypes, StringBuffer pattern) {
final char character = ignoreCase ? fileName.toLowerCase().charAt(len - 1) : fileName.charAt(len - 1);
if (character == 0) {
return;
}
int min = 0;
int max = numEntries - 1;
while (max >= min && len >= 0) {
int mid = (min + max) / 2;
char matchChar = (char) content.getInt(offset + (12 * mid));
if (ignoreCase)
matchChar = Character.toLowerCase(matchChar);
if (matchChar < character) {
min = mid + 1;
} else if (matchChar > character) {
max = mid - 1;
} else {
len--;
// first leaf nodes (matchChar==0) then tree nodes
final int numChildren = content.getInt(offset + (12 * mid) + 4);
final int firstChildOffset = content.getInt(offset + (12 * mid) + 8);
if (len > 0) {
pattern.append(matchChar);
lookupGlobNodeSuffix(fileName, numChildren, firstChildOffset, ignoreCase, len, mimeTypes, pattern);
}
// if the name did not match a longer pattern, try to match this one
if (mimeTypes.isEmpty()) {
for (int i = 0; i < numChildren; i++) {
final int childOffset = firstChildOffset + (12 * i);
if (content.getInt(childOffset) != 0) {
// not a leaf node anymore
break;
}
final int mimeOffset = content.getInt(childOffset + 4);
final int weightNFlags = content.getInt(childOffset + 8);
final int weight = weightNFlags & 0xFF;
final boolean cs = (weightNFlags & 0x100) != 0;
if (!(cs && ignoreCase))
mimeTypes.add(new WeightedMimeType(getMimeType(mimeOffset), pattern.toString(), weight));
}
}
return;
}
}
}
static class WeightedMimeType extends MimeType {
private static final long serialVersionUID = 1L;
String pattern;
int weight;
WeightedMimeType(String mimeType, String pattern, int weight) {
super(mimeType);
this.pattern = pattern;
this.weight = weight;
}
}
private int getMaxExtents() {
return content.getInt(getMagicListOffset() + 4);
}
private String aliasLookup(String alias) {
int aliasListOffset = getAliasListOffset();
int min = 0;
int max = content.getInt(aliasListOffset) - 1;
while (max >= min) {
int mid = (min + max) / 2;
// content.position((aliasListOffset + 4) + (mid * 8));
int aliasOffset = content.getInt((aliasListOffset + 4) + (mid * 8));
int mimeOffset = content.getInt((aliasListOffset + 4) + (mid * 8) + 4);
int cmp = getMimeType(aliasOffset).compareTo(alias);
if (cmp < 0) {
min = mid + 1;
} else if (cmp > 0) {
max = mid - 1;
} else {
return getMimeType(mimeOffset);
}
}
return null;
}
private String unaliasMimeType(String mimeType) {
String lookup = aliasLookup(mimeType);
return lookup == null ? mimeType : lookup;
}
private boolean isMimeTypeSubclass(String mimeType, String subClass) {
String umimeType = unaliasMimeType(mimeType);
String usubClass = unaliasMimeType(subClass);
MimeType _mimeType = new MimeType(umimeType);
MimeType _subClass = new MimeType(usubClass);
if (umimeType.compareTo(usubClass) == 0) {
return true;
}
if (isSuperType(usubClass) && (_mimeType.getMediaType().equals(_subClass.getMediaType()))) {
return true;
}
// Handle special cases text/plain and application/octet-stream
if (usubClass.equals("text/plain") && _mimeType.getMediaType().equals("text")) {
return true;
}
if (usubClass.equals("application/octet-stream")) {
return true;
}
int parentListOffset = getParentListOffset();
int numParents = content.getInt(parentListOffset);
int min = 0;
int max = numParents - 1;
while (max >= min) {
int med = (min + max) / 2;
int offset = content.getInt((parentListOffset + 4) + (8 * med));
String parentMime = getMimeType(offset);
int cmp = parentMime.compareTo(umimeType);
if (cmp < 0) {
min = med + 1;
} else if (cmp > 0) {
max = med - 1;
} else {
offset = content.getInt((parentListOffset + 4) + (8 * med) + 4);
int _numParents = content.getInt(offset);
for (int i = 0; i < _numParents; i++) {
int parentOffset = content.getInt((offset + 4) + (4 * i));
if (isMimeTypeSubclass(getMimeType(parentOffset), usubClass)) {
return true;
}
}
break;
}
}
return false;
}
private boolean isSuperType(String mimeType) {
return mimeType.endsWith("/*");
}
private int getGenericIconListOffset() {
return content.getInt(36);
}
private int getIconListOffset() {
return content.getInt(32);
}
private int getNameSpaceListOffset() {
return content.getInt(28);
}
private int getMagicListOffset() {
return content.getInt(24);
}
private int getGlobListOffset() {
return content.getInt(20);
}
private int getReverseSuffixTreeOffset() {
return content.getInt(16);
}
private int getLiteralListOffset() {
return content.getInt(12);
}
private int getParentListOffset() {
return content.getInt(8);
}
private int getAliasListOffset() {
return content.getInt(4);
}
private short getMinorVersion() {
return content.getShort(2);
}
private short getMajorVersion() {
return content.getShort(0);
}
private String getMimeType(int offset) {
return getString(offset);
}
private String getString(int offset) {
return getString(offset, false);
}
private String getString(int offset, boolean regularExpression) {
int position = content.position();
content.position(offset);
StringBuffer buf = new StringBuffer();
char c = 0;
while ((c = (char) content.get()) != 0) {
if (regularExpression) {
switch (c) {
case '.':
buf.append("\\");
break;
case '*':
case '+':
case '?':
buf.append(".");
}
}
buf.append(c);
}
// Reset position
content.position(position);
if (regularExpression) {
buf.insert(0, '^');
buf.append('$');
}
return buf.toString();
}
private Collection<String> _getMimeTypes(Collection<String> mimeTypes, final InputStream in) throws IOException {
if (mimeTypes.isEmpty() || mimeTypes.size() > 1) {
Collection<String> _mimeTypes = getMimeTypesInputStream(in);
if (!_mimeTypes.isEmpty()) {
if (!mimeTypes.isEmpty()) {
// more than one glob matched
// Check for same mime type
for (final String mimeType : mimeTypes) {
if (_mimeTypes.contains(mimeType)) {
// mimeTypes = new ArrayList();
mimeTypes.add(mimeType);
// return mimeTypes;
}
// Check for mime type subtype
for (final String _mimeType : _mimeTypes) {
if (isMimeTypeSubclass(mimeType, _mimeType)) {
// mimeTypes = new ArrayList();
mimeTypes.add(mimeType);
// return mimeTypes;
}
}
}
} else {
// No globs matched but we have magic matches
return _mimeTypes;
}
}
}
return mimeTypes;
}
}