|
18 | 18 |
|
19 | 19 | import java.io.IOException; |
20 | 20 | import java.io.InputStream; |
| 21 | +import java.nio.channels.Channels; |
| 22 | +import java.nio.channels.SeekableByteChannel; |
21 | 23 | import java.nio.charset.StandardCharsets; |
22 | 24 | import java.util.ArrayList; |
23 | 25 | import java.util.Collections; |
|
35 | 37 | import org.apache.tika.exception.TikaMemoryLimitException; |
36 | 38 | import org.apache.tika.extractor.EmbeddedDocumentExtractor; |
37 | 39 | import org.apache.tika.extractor.EmbeddedDocumentUtil; |
| 40 | +import org.apache.tika.io.CacheMemoryBudget; |
38 | 41 | import org.apache.tika.io.EndianUtils; |
| 42 | +import org.apache.tika.io.TemporaryResources; |
39 | 43 | import org.apache.tika.io.TikaInputStream; |
40 | 44 | import org.apache.tika.metadata.Metadata; |
41 | 45 | import org.apache.tika.metadata.TikaCoreProperties; |
@@ -86,36 +90,54 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata |
86 | 90 |
|
87 | 91 | EmbeddedDocumentExtractor ex = EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context); |
88 | 92 |
|
| 93 | + //the data fork is handed over as a region of this stream, which needs a |
| 94 | + //seekable view after the header has been read sequentially |
| 95 | + tis.enableRewind(context.get(CacheMemoryBudget.class)); |
89 | 96 | short numEntries = readThroughNumEntries(tis); |
90 | | - long bytesRead = 26; |
91 | 97 | List<FieldInfo> fieldInfoList = getSortedFieldInfoList(tis, numEntries); |
92 | | - bytesRead += 12 * numEntries; |
93 | 98 | Metadata embeddedMetadata = Metadata.newInstance(context); |
94 | | - bytesRead = processFieldEntries(tis, fieldInfoList, embeddedMetadata, bytesRead); |
| 99 | + processFieldEntries(tis, fieldInfoList, embeddedMetadata, 26 + 12L * numEntries); |
95 | 100 | FieldInfo contentFieldInfo = getContentFieldInfo(fieldInfoList); |
96 | 101 | XHTMLContentHandler xhtml = new XHTMLContentHandler(handler, metadata, context); |
97 | 102 | xhtml.startDocument(); |
98 | | - if (contentFieldInfo != null) { |
99 | | - long diff = contentFieldInfo.offset - bytesRead; |
100 | | - IOUtils.skipFully(tis, diff); |
101 | | - if (ex.shouldParseEmbedded(embeddedMetadata, context)) { |
102 | | - // Use BoundedInputStream to limit bytes read, then spool to temp file |
103 | | - // for complete isolation from parent stream (reset() goes to embedded start) |
104 | | - BoundedInputStream bounded = |
105 | | - BoundedInputStream.builder() |
106 | | - .setInputStream(tis) |
107 | | - .setMaxCount(contentFieldInfo.length) |
108 | | - .get(); |
109 | | - try (TikaInputStream inner = TikaInputStream.get(bounded)) { |
110 | | - inner.getPath(); |
111 | | - ex.parseEmbedded(inner, xhtml, embeddedMetadata, context, true); |
112 | | - } |
| 103 | + if (contentFieldInfo != null && ex.shouldParseEmbedded(embeddedMetadata, context)) { |
| 104 | + //re-opened from the channel on rewind: a digest re-reads the fork in place |
| 105 | + //instead of the copy-and-spool that getPath() used to force on every parse |
| 106 | + long offset = contentFieldInfo.offset; |
| 107 | + long length = contentFieldInfo.length; |
| 108 | + try (TikaInputStream inner = TikaInputStream.get(() -> region(tis, offset, length), |
| 109 | + new TemporaryResources(), null)) { |
| 110 | + ex.parseEmbedded(inner, xhtml, embeddedMetadata, context, true); |
113 | 111 | } |
114 | 112 | } |
115 | 113 | xhtml.endDocument(); |
116 | 114 |
|
117 | 115 | } |
118 | 116 |
|
| 117 | + /** |
| 118 | + * The data fork as a fresh stream over the parent's seekable channel: in memory |
| 119 | + * when the parent is, from its file when it has one. The offset and length are |
| 120 | + * the file's own claims; a region past the end simply reads as empty. |
| 121 | + */ |
| 122 | + private static InputStream region(TikaInputStream tis, long offset, long length) |
| 123 | + throws IOException { |
| 124 | + if (offset < 0 || length < 0) { |
| 125 | + throw new IOException("AppleSingle data fork out of range: offset=" + offset + |
| 126 | + " length=" + length); |
| 127 | + } |
| 128 | + SeekableByteChannel channel = tis.getSeekableByteChannel(); |
| 129 | + try { |
| 130 | + channel.position(offset); |
| 131 | + return BoundedInputStream.builder() |
| 132 | + .setInputStream(Channels.newInputStream(channel)) |
| 133 | + .setMaxCount(length) |
| 134 | + .get(); |
| 135 | + } catch (IOException e) { |
| 136 | + channel.close(); |
| 137 | + throw e; |
| 138 | + } |
| 139 | + } |
| 140 | + |
119 | 141 | private FieldInfo getContentFieldInfo(List<FieldInfo> fieldInfoList) { |
120 | 142 | for (FieldInfo fieldInfo : fieldInfoList) { |
121 | 143 | if (fieldInfo.entryId == 1) { |
|
0 commit comments