AppleSingleFileParser.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
package org.apache.tika.parser.apple;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.Channels;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.BoundedInputStream;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.apache.tika.annotation.TikaComponent;
import org.apache.tika.exception.TikaException;
import org.apache.tika.exception.TikaMemoryLimitException;
import org.apache.tika.extractor.EmbeddedDocumentExtractor;
import org.apache.tika.extractor.EmbeddedDocumentUtil;
import org.apache.tika.io.CacheMemoryBudget;
import org.apache.tika.io.EndianUtils;
import org.apache.tika.io.TemporaryResources;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.mime.MediaType;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.Parser;
import org.apache.tika.sax.XHTMLContentHandler;
/**
* Parser that strips the header off of AppleSingle and AppleDouble
* files.
* <p>
* See <a href="http://kaiser-edv.de/documents/AppleSingle_AppleDouble.pdf">spec document</a>.
*/
@TikaComponent
public class AppleSingleFileParser implements Parser {
private static final int MAX_FIELD_LENGTH = 1_073_741_824;
/**
* Entry types
*/
private static final int DATA_FORK = 1;
private static final int RESOURCE_FORK = 2;
private static final int REAL_NAME = 3;
private static final int COMMENT = 4;
private static final int ICON_BW = 5;
private static final int ICON_COLOR = 6;
//7?!
private static final int FILE_DATES_INFO = 8;
private static final int FINDER_INFO = 9;
private static final int MACINTOSH_FILE_INFO = 10;
private static final int PRODOS_FILE_INFO = 11;
private static final int MSDOS_FILE_INFO = 12;
private static final int SHORT_NAME = 13;
private static final int AFP_FILE_INFO = 14;
private static final int DIRECTORY_ID = 15;
private static final Set<MediaType> SUPPORTED_TYPES =
Collections.singleton(MediaType.application("applefile"));
public Set<MediaType> getSupportedTypes(ParseContext context) {
return SUPPORTED_TYPES;
}
@Override
public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata,
ParseContext context) throws IOException, SAXException, TikaException {
EmbeddedDocumentExtractor ex = EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context);
//the data fork is handed over as a region of this stream, which needs a
//seekable view after the header has been read sequentially
tis.enableRewind(context.get(CacheMemoryBudget.class));
short numEntries = readThroughNumEntries(tis);
List<FieldInfo> fieldInfoList = getSortedFieldInfoList(tis, numEntries);
Metadata embeddedMetadata = Metadata.newInstance(context);
processFieldEntries(tis, fieldInfoList, embeddedMetadata, 26 + 12L * numEntries);
FieldInfo contentFieldInfo = getContentFieldInfo(fieldInfoList);
XHTMLContentHandler xhtml = new XHTMLContentHandler(handler, metadata, context);
xhtml.startDocument();
if (contentFieldInfo != null && ex.shouldParseEmbedded(embeddedMetadata, context)) {
//re-opened from the channel on rewind: a digest re-reads the fork in place
//instead of the copy-and-spool that getPath() used to force on every parse
long offset = contentFieldInfo.offset;
long length = contentFieldInfo.length;
try (TikaInputStream inner = TikaInputStream.get(() -> region(tis, offset, length),
new TemporaryResources(), null)) {
ex.parseEmbedded(inner, xhtml, embeddedMetadata, context, true);
}
}
xhtml.endDocument();
}
/**
* The data fork as a fresh stream over the parent's seekable channel: in memory
* when the parent is, from its file when it has one. The offset and length are
* the file's own claims; a region past the end simply reads as empty.
*/
private static InputStream region(TikaInputStream tis, long offset, long length)
throws IOException {
if (offset < 0 || length < 0) {
throw new IOException("AppleSingle data fork out of range: offset=" + offset +
" length=" + length);
}
SeekableByteChannel channel = tis.getSeekableByteChannel();
try {
channel.position(offset);
return BoundedInputStream.builder()
.setInputStream(Channels.newInputStream(channel))
.setMaxCount(length)
.get();
} catch (IOException e) {
channel.close();
throw e;
}
}
private FieldInfo getContentFieldInfo(List<FieldInfo> fieldInfoList) {
for (FieldInfo fieldInfo : fieldInfoList) {
if (fieldInfo.entryId == 1) {
return fieldInfo;
}
}
return null;
}
private long processFieldEntries(InputStream tis, List<FieldInfo> fieldInfoList,
Metadata embeddedMetadata, long bytesRead)
throws IOException, TikaException {
byte[] buffer = null;
for (FieldInfo f : fieldInfoList) {
long diff = f.offset - bytesRead;
//just in case
IOUtils.skipFully(tis, diff);
bytesRead += diff;
if (f.entryId == REAL_NAME) {
if (f.length > MAX_FIELD_LENGTH) {
throw new TikaMemoryLimitException(f.length, MAX_FIELD_LENGTH);
}
buffer = new byte[(int) f.length];
IOUtils.readFully(tis, buffer);
bytesRead += f.length;
String originalFileName =
new String(buffer, 0, buffer.length, StandardCharsets.US_ASCII);
embeddedMetadata.set(TikaCoreProperties.ORIGINAL_RESOURCE_NAME, originalFileName);
} else if (f.entryId != DATA_FORK) {
IOUtils.skipFully(tis, f.length);
bytesRead += f.length;
}
}
return bytesRead;
}
private List<FieldInfo> getSortedFieldInfoList(InputStream tis, short numEntries)
throws IOException, TikaException {
//this is probably overkill. I'd hope that these were already
//in order. This ensures it.
List<FieldInfo> fieldInfoList = new ArrayList<>(numEntries);
for (int i = 0; i < numEntries; i++) {
//convert 32-bit unsigned ints to longs
fieldInfoList.add(new FieldInfo(EndianUtils.readUIntBE(tis), //entry id
EndianUtils.readUIntBE(tis), //offset
EndianUtils.readUIntBE(tis) //length
));
}
if (fieldInfoList.size() == 0) {
throw new TikaException("AppleSingleFile missing field info");
}
//make absolutely sure these are in order!
fieldInfoList.sort(Comparator.comparingLong(fieldInfo -> fieldInfo.offset));
return fieldInfoList;
}
//read through header until you hit the number of entries
private short readThroughNumEntries(InputStream tis) throws TikaException, IOException {
//mime
EndianUtils.readIntBE(tis);
//version
long version = EndianUtils.readIntBE(tis);
if (version != 0x00020000) {
throw new TikaException("Version should have been 0x00020000, but was:" + version);
}
IOUtils.skipFully(tis, 16);//filler
return EndianUtils.readShortBE(tis);//number of entries
}
private static class FieldInfo {
private final long entryId;
private final long offset;
private final long length;
private FieldInfo(long entryId, long offset, long length) {
this.entryId = entryId;
this.offset = offset;
this.length = length;
}
}
}