This is an automated email from the ASF dual-hosted git repository.
tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git
The following commit(s) were added to refs/heads/main by this push:
new b8ed3fbb6f TIKA-4878: ole, ooxml (#3132)
b8ed3fbb6f is described below
commit b8ed3fbb6fe06841bb9cac377be9f71f62ad92ae
Author: Tim Allison <[email protected]>
AuthorDate: Fri Sep 4 14:06:41 2026 -0400
TIKA-4878: ole, ooxml (#3132)
---
CHANGES.txt | 9 +
.../parser/microsoft/AbstractPOIFSExtractor.java | 12 +-
.../tika/parser/microsoft/HSLFExtractor.java | 16 +-
.../microsoft/ooxml/AbstractOOXMLExtractor.java | 58 +++---
.../ooxml/SXWPFWordExtractorDecorator.java | 4 +-
.../microsoft/ooxml/xps/XPSExtractorDecorator.java | 5 +-
.../microsoft/EmbeddedObjectsNoTempFileTest.java | 209 +++++++++++++++++++++
7 files changed, 272 insertions(+), 41 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index a3aaaebafd..8c2568710d 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,14 @@
Release 4.1.0 - unreleased
+ * Embedded objects in Office documents are re-opened from their container
+ instead of cached: every OOXML part (pictures, media, attachments), the
+ OLE 2.0 package inside an OOXML part, the CONTENTS entry of an OLE 2.0
+ object in a binary Office file, embedded objects in .ppt, XPS page
+ images and Word EMF icons. Digesting an embedded document rewinds it;
+ the cached copy that made possible cost heap for the whole object and,
+ past the cache budget or the 1 MB floor, a temp file. The container
+ hands the bytes back on demand, so neither is needed (TIKA-4878).
+
* PDF attachments, PDF XMP packets, 3D on-instantiate scripts and PST
attachments are re-opened from their document instead of cached when
the embedded-document extractor rewinds them (digesting does, for every
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/AbstractPOIFSExtractor.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/AbstractPOIFSExtractor.java
index 28dbffa236..fb5f04b50c 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/AbstractPOIFSExtractor.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/AbstractPOIFSExtractor.java
@@ -42,6 +42,7 @@ import org.apache.tika.exception.TikaException;
import org.apache.tika.extractor.EmbeddedDocumentExtractor;
import org.apache.tika.extractor.EmbeddedDocumentUtil;
import org.apache.tika.io.BoundedInputStream;
+import org.apache.tika.io.TemporaryResources;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.HttpHeaders;
import org.apache.tika.metadata.Metadata;
@@ -300,16 +301,19 @@ abstract class AbstractPOIFSExtractor {
}
int length = contentsEntry.getSize();
- DocumentInputStream inp = null;
+ //open once now so a broken entry is recorded here, not mid-parse
try {
- inp = new DocumentInputStream(contentsEntry);
+ new DocumentInputStream(contentsEntry).close();
} catch (SecurityException e) {
throw e;
} catch (Exception e) {
EmbeddedDocumentUtil.recordEmbeddedStreamException(e,
parentMetadata, context);
return;
}
- try (TikaInputStream tis = TikaInputStream.get(inp)) {
+ //the entry is in the container already: re-open it on rewind instead
of
+ //caching a copy that a digest of a large object would spill to disk
+ try (TikaInputStream tis = TikaInputStream.get(
+ () -> new DocumentInputStream(contentsEntry), new
TemporaryResources(), null)) {
// Try to work out what it is
MediaType mediaType = getDetector().detect(tis, metadata, context);
String extension = type.getExtension();
@@ -327,8 +331,6 @@ abstract class AbstractPOIFSExtractor {
metadata.set(TikaCoreProperties.RESOURCE_NAME_EXTENSION_INFERRED,
true);
metadata.set(HttpHeaders.CONTENT_LENGTH, Integer.toString(length));
parseEmbedded(parentDir, tis, xhtml, metadata, outputHtml);
- } finally {
- inp.close();
}
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/HSLFExtractor.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/HSLFExtractor.java
index 197a920bb3..b3c0e76adc 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/HSLFExtractor.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/HSLFExtractor.java
@@ -17,7 +17,6 @@
package org.apache.tika.parser.microsoft;
import java.io.IOException;
-import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
@@ -60,6 +59,7 @@ import org.xml.sax.helpers.AttributesImpl;
import org.apache.tika.exception.EncryptedDocumentException;
import org.apache.tika.exception.TikaException;
import org.apache.tika.extractor.EmbeddedDocumentUtil;
+import org.apache.tika.io.TemporaryResources;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.Office;
@@ -232,7 +232,8 @@ public class HSLFExtractor extends AbstractPOIFSExtractor {
i, getDetectedMediaType(d));
inferredExtension = true;
}
- try (TikaInputStream tis =
TikaInputStream.get(d.getInputStream())) {
+ try (TikaInputStream tis = TikaInputStream.get(d::getInputStream,
+ new TemporaryResources(), null)) {
if (FileMagic.valueOf(tis) == FileMagic.OLE2) {
try (POIFSFileSystem pfs = new POIFSFileSystem(tis)) {
//coz ppts can have empty pfs...shrug...
@@ -664,23 +665,24 @@ public class HSLFExtractor extends AbstractPOIFSExtractor
{
attributes.addAttribute("", "id", "id", "CDATA", objID);
xhtml.startElement("div", attributes);
xhtml.endElement("div");
- InputStream dataStream = null;
+ //open once now so a broken record is recorded here, not
mid-parse
try {
- dataStream = data.getInputStream();
+ data.getInputStream().close();
} catch (Exception e) {
EmbeddedDocumentUtil.recordEmbeddedStreamException(e,
parentMetadata, context);
continue;
}
- handleDataStream(dataStream, objID, oleShape.getProgId(),
xhtml);
+ handleDataStream(data, objID, oleShape.getProgId(), xhtml);
}
}
}
}
- private void handleDataStream(InputStream dataStream, String objID, String
progId,
+ private void handleDataStream(HSLFObjectData data, String objID, String
progId,
XHTMLContentHandler xhtml) {
//TODO -- inject progId into the metadata of the embedded file
- try (TikaInputStream tis = TikaInputStream.get(dataStream)) {
+ try (TikaInputStream tis = TikaInputStream.get(data::getInputStream,
+ new TemporaryResources(), null)) {
String mediaType = null;
if ("Excel.Chart.8".equals(progId)) {
mediaType = "application/vnd.ms-excel";
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/AbstractOOXMLExtractor.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/AbstractOOXMLExtractor.java
index f49b1eadcb..96acd30d40 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/AbstractOOXMLExtractor.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/AbstractOOXMLExtractor.java
@@ -52,6 +52,7 @@ import org.apache.tika.exception.WriteLimitReachedException;
import org.apache.tika.extractor.EmbeddedDocumentExtractor;
import org.apache.tika.extractor.EmbeddedDocumentUtil;
import org.apache.tika.io.FilenameUtils;
+import org.apache.tika.io.TemporaryResources;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.HttpHeaders;
import org.apache.tika.metadata.Metadata;
@@ -204,30 +205,29 @@ public abstract class AbstractOOXMLExtractor implements
OOXMLExtractor {
if (tPart == null) {
continue;
}
- try (InputStream tStream = tPart.getInputStream()) {
- Metadata thumbnailMetadata = Metadata.newInstance(context);
- String thumbName = tPart.getPartName().getName();
- thumbnailMetadata.set(TikaCoreProperties.INTERNAL_PATH,
thumbName);
- thumbnailMetadata.set(TikaCoreProperties.RESOURCE_NAME_KEY,
- FilenameUtils.getName(thumbName));
-
- AttributesImpl attributes = new AttributesImpl();
- attributes.addAttribute(XHTML, "class", "class", "CDATA",
"embedded");
- attributes.addAttribute(XHTML, "id", "id", "CDATA",
thumbName);
- handler.startElement(XHTML, "div", "div", attributes);
- handler.endElement(XHTML, "div", "div");
-
-
thumbnailMetadata.set(TikaCoreProperties.EMBEDDED_RELATIONSHIP_ID, thumbName);
- thumbnailMetadata.set(HttpHeaders.CONTENT_TYPE,
tPart.getContentType());
- thumbnailMetadata.set(TikaCoreProperties.TITLE,
tPart.getPartName().getName());
-
thumbnailMetadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE,
-
TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.name());
-
- if
(embeddedExtractor.shouldParseEmbedded(thumbnailMetadata, context)) {
- try (TikaInputStream tis =
TikaInputStream.get(tStream)) {
- embeddedExtractor.parseEmbedded(tis,
- new EmbeddedContentHandler(handler),
thumbnailMetadata, context, false);
- }
+ Metadata thumbnailMetadata = Metadata.newInstance(context);
+ String thumbName = tPart.getPartName().getName();
+ thumbnailMetadata.set(TikaCoreProperties.INTERNAL_PATH,
thumbName);
+ thumbnailMetadata.set(TikaCoreProperties.RESOURCE_NAME_KEY,
+ FilenameUtils.getName(thumbName));
+
+ AttributesImpl attributes = new AttributesImpl();
+ attributes.addAttribute(XHTML, "class", "class", "CDATA",
"embedded");
+ attributes.addAttribute(XHTML, "id", "id", "CDATA", thumbName);
+ handler.startElement(XHTML, "div", "div", attributes);
+ handler.endElement(XHTML, "div", "div");
+
+
thumbnailMetadata.set(TikaCoreProperties.EMBEDDED_RELATIONSHIP_ID, thumbName);
+ thumbnailMetadata.set(HttpHeaders.CONTENT_TYPE,
tPart.getContentType());
+ thumbnailMetadata.set(TikaCoreProperties.TITLE,
tPart.getPartName().getName());
+
thumbnailMetadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE,
+
TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.name());
+
+ if (embeddedExtractor.shouldParseEmbedded(thumbnailMetadata,
context)) {
+ try (TikaInputStream tis =
TikaInputStream.get(tPart::getInputStream,
+ new TemporaryResources(), null)) {
+ embeddedExtractor.parseEmbedded(tis,
+ new EmbeddedContentHandler(handler),
thumbnailMetadata, context, false);
}
}
}
@@ -400,7 +400,8 @@ public abstract class AbstractOOXMLExtractor implements
OOXMLExtractor {
//OLE 2.0
updateMetadata(metadata, embeddedPartMetadata);
- tis =
TikaInputStream.get(fs.createDocumentInputStream(packageEntryName));
+ tis = TikaInputStream.get(() ->
fs.createDocumentInputStream(packageEntryName),
+ new TemporaryResources(), null);
if (embeddedExtractor.shouldParseEmbedded(metadata, context)) {
embeddedExtractor
.parseEmbedded(tis, xhtml, metadata, context,
true);
@@ -437,10 +438,10 @@ public abstract class AbstractOOXMLExtractor implements
OOXMLExtractor {
} catch (IOException e) {
EmbeddedDocumentUtil.recordEmbeddedStreamException(e,
parentMetadata, context);
} finally {
- fs.close();
if (tis != null) {
tis.close();
}
+ fs.close();
}
}
@@ -506,7 +507,10 @@ public abstract class AbstractOOXMLExtractor implements
OOXMLExtractor {
// Call the recursing handler
if (embeddedExtractor.shouldParseEmbedded(metadata, context)) {
- try (TikaInputStream tis =
TikaInputStream.get(part.getInputStream())) {
+ //the part is in the package already: re-open it on rewind instead
of
+ //caching a copy that a digest of a large part would spill to disk
+ try (TikaInputStream tis =
TikaInputStream.get(part::getInputStream,
+ new TemporaryResources(), null)) {
embeddedExtractor
.parseEmbedded(tis, xhtml, metadata, context, true);
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/SXWPFWordExtractorDecorator.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/SXWPFWordExtractorDecorator.java
index d72ac8f225..f7a00c1dc9 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/SXWPFWordExtractorDecorator.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/SXWPFWordExtractorDecorator.java
@@ -40,6 +40,7 @@ import org.xml.sax.helpers.DefaultHandler;
import org.apache.tika.exception.TikaException;
import org.apache.tika.exception.WriteLimitReachedException;
import org.apache.tika.extractor.EmbeddedDocumentUtil;
+import org.apache.tika.io.TemporaryResources;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.Office;
@@ -401,7 +402,8 @@ public class SXWPFWordExtractorDecorator extends
AbstractOOXMLExtractor {
continue;
}
if ("image/x-emf".equals(emfPart.getContentType())) {
- try (TikaInputStream tis =
TikaInputStream.get(emfPart.getInputStream())) {
+ try (TikaInputStream tis =
TikaInputStream.get(emfPart::getInputStream,
+ new TemporaryResources(), null)) {
EMFParser p = new EMFParser();
Metadata m = Metadata.newInstance(context);
p.parse(tis, new
org.apache.tika.sax.ToTextContentHandler(), m, context);
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/xps/XPSExtractorDecorator.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/xps/XPSExtractorDecorator.java
index e4de2f7f82..844bfb3700 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/xps/XPSExtractorDecorator.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/xps/XPSExtractorDecorator.java
@@ -39,6 +39,7 @@ import org.xml.sax.helpers.DefaultHandler;
import org.apache.tika.exception.TikaException;
import org.apache.tika.extractor.EmbeddedDocumentExtractor;
import org.apache.tika.extractor.EmbeddedDocumentUtil;
+import org.apache.tika.io.TemporaryResources;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.ParseContext;
@@ -83,7 +84,9 @@ public class XPSExtractorDecorator extends
AbstractOOXMLExtractor {
if (zipEntry == null) {
throw new TikaException("Couldn't find required zip entry: " +
zipPath);
}
- return TikaInputStream.get(zipEntrySource.getInputStream(zipEntry));
+ ZipArchiveEntry entry = zipEntry;
+ return TikaInputStream.get(() -> zipEntrySource.getInputStream(entry),
+ new TemporaryResources(), null);
}
@Override
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/EmbeddedObjectsNoTempFileTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/EmbeddedObjectsNoTempFileTest.java
new file mode 100644
index 0000000000..94bbeca7ee
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/EmbeddedObjectsNoTempFileTest.java
@@ -0,0 +1,209 @@
+/*
+ * 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.microsoft;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+import org.apache.poi.openxml4j.opc.OPCPackage;
+import org.apache.poi.openxml4j.opc.PackagePart;
+import org.apache.poi.openxml4j.opc.PackagingURIHelper;
+import org.apache.poi.poifs.filesystem.DirectoryEntry;
+import org.apache.poi.poifs.filesystem.Entry;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.helpers.DefaultHandler;
+
+import org.apache.tika.TikaTest;
+import org.apache.tika.extractor.EmbeddedDocumentExtractor;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.parser.Parser;
+import org.apache.tika.parser.microsoft.ooxml.OOXMLParser;
+
+/**
+ * An embedded object's bytes are already in its container. Rewinding the
+ * stream handed to the embedded-document extractor -- which digesting does for
+ * every embedded document -- must re-open the object from the container, not
+ * cache a copy of it and spill that copy to a temp file.
+ * <p>
+ * Each payload is over the 1 MB a cache keeps in memory, so a cached stream
has
+ * to spill to rewind and the difference is observable. The assertion is on the
+ * stream the extractor is handed: the parser owns each child's
+ * {@code TemporaryResources}, so a watched directory would pass either way.
+ * Fixtures are built at test time by rewriting one part of a small document.
+ */
+public class EmbeddedObjectsNoTempFileTest extends TikaTest {
+
+ private static final int PAYLOAD_LENGTH = 2 * 1024 * 1024;
+
+ @TempDir
+ Path tempDir;
+
+ /** An OOXML part reached through {@code handleEmbeddedFile}: every
picture, media
+ * file and attachment in a docx, pptx or xlsx. */
+ @Test
+ public void testOoxmlPartIsNotSpooled() throws Exception {
+ Path docx = copyOf("EmbeddedPDF.docx", "picture.docx");
+ overwritePart(docx, "/word/media/image1.emf", payload());
+ RecordingExtractor extractor = parse(docx, new OOXMLParser());
+ extractor.assertPayloadReadWithoutSpooling();
+ }
+
+ /** An OLE 2.0 package inside an OOXML part, reached through {@code
handleEmbeddedOLE}:
+ * the container's {@code Package} entry is re-opened from the {@code
POIFSFileSystem}. */
+ @Test
+ public void testOoxmlOlePackageIsNotSpooled() throws Exception {
+ Path docx = copyOf("EmbeddedPDF.docx", "ole.docx");
+ overwritePart(docx, "/word/embeddings/oleObject1.bin",
+ ole2(new String[]{"Ole", "Package"}, new byte[][]{new
byte[20], payload()}));
+ RecordingExtractor extractor = parse(docx, new OOXMLParser());
+ extractor.assertPayloadReadWithoutSpooling();
+ }
+
+ /** An OLE 2.0 object in a binary Office file, reached through {@code
handleCompObj}:
+ * the {@code CONTENTS} entry is re-opened from its {@code
DirectoryEntry}. */
+ @Test
+ public void testOle2ContentsIsNotSpooled() throws Exception {
+ Path xls = tempDir.resolve("contents.xls");
+ try (InputStream is =
getResourceAsStream("/test-documents/testExcel_embeddedPDF.xls");
+ POIFSFileSystem fs = new POIFSFileSystem(is);
+ OutputStream out = Files.newOutputStream(xls)) {
+ DirectoryEntry object = directoryWith(fs.getRoot(), "CONTENTS");
+ object.getEntry("CONTENTS").delete();
+ object.createDocument("CONTENTS", new
ByteArrayInputStream(payload()));
+ fs.writeFilesystem(out);
+ }
+ RecordingExtractor extractor = parse(xls, new OfficeParser());
+ extractor.assertPayloadReadWithoutSpooling();
+ }
+
+ private Path copyOf(String fixture, String name) throws IOException {
+ Path copy = tempDir.resolve(name);
+ try (InputStream is = getResourceAsStream("/test-documents/" +
fixture)) {
+ Files.copy(is, copy);
+ }
+ return copy;
+ }
+
+ /** Replaces one part's bytes in place; its name, content type and
relationships stay. */
+ private static void overwritePart(Path docx, String partName, byte[]
bytes) throws Exception {
+ try (OPCPackage pkg = OPCPackage.open(docx.toFile())) {
+ PackagePart part =
pkg.getPart(PackagingURIHelper.createPartName(partName));
+ try (OutputStream out = part.getOutputStream()) {
+ out.write(bytes);
+ }
+ }
+ }
+
+ private RecordingExtractor parse(Path file, Parser parser) throws
Exception {
+ RecordingExtractor extractor = new RecordingExtractor();
+ ParseContext context = new ParseContext();
+ context.set(EmbeddedDocumentExtractor.class, extractor);
+ Metadata metadata = new Metadata();
+ try (TikaInputStream tis = TikaInputStream.get(file, metadata)) {
+ parser.parse(tis, new DefaultHandler(), metadata, context);
+ }
+ return extractor;
+ }
+
+ /** Looks like a PDF to detection and is otherwise filler. */
+ private static byte[] payload() {
+ byte[] bytes = new byte[PAYLOAD_LENGTH];
+ new Random(4878).nextBytes(bytes);
+ byte[] header = "%PDF-1.4\n".getBytes(StandardCharsets.US_ASCII);
+ System.arraycopy(header, 0, bytes, 0, header.length);
+ return bytes;
+ }
+
+ private static byte[] ole2(String[] names, byte[][] contents) throws
IOException {
+ try (POIFSFileSystem fs = new POIFSFileSystem()) {
+ for (int i = 0; i < names.length; i++) {
+ fs.getRoot().createDocument(names[i], new
ByteArrayInputStream(contents[i]));
+ }
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ fs.writeFilesystem(out);
+ return out.toByteArray();
+ }
+ }
+
+ private static DirectoryEntry directoryWith(DirectoryEntry dir, String
entryName)
+ throws IOException {
+ for (Entry entry : dir) {
+ if (entry instanceof DirectoryEntry child) {
+ if (child.hasEntry(entryName)) {
+ return child;
+ }
+ try {
+ return directoryWith(child, entryName);
+ } catch (IOException notHere) {
+ // keep looking in the siblings
+ }
+ }
+ }
+ throw new IOException("no directory holding " + entryName + " under "
+ dir.getName());
+ }
+
+ /**
+ * Rewinds each embedded stream the way a digester does, then records
whether
+ * that left it backed by a temp file and how many bytes it still yields.
+ */
+ private static class RecordingExtractor implements
EmbeddedDocumentExtractor {
+ private final List<Boolean> spooled = new ArrayList<>();
+ private final List<Integer> lengths = new ArrayList<>();
+
+ @Override
+ public boolean shouldParseEmbedded(Metadata metadata, ParseContext
context) {
+ return true;
+ }
+
+ @Override
+ public void parseEmbedded(TikaInputStream stream, ContentHandler
handler,
+ Metadata metadata, ParseContext context,
boolean outputHtml)
+ throws IOException {
+ stream.enableRewind();
+ stream.readAllBytes();
+ stream.rewind();
+ spooled.add(stream.hasFile());
+ lengths.add(stream.readAllBytes().length);
+ }
+
+ void assertPayloadReadWithoutSpooling() {
+ assertTrue(lengths.contains(PAYLOAD_LENGTH),
+ "the payload reached the extractor in full; saw " +
lengths);
+ for (int i = 0; i < spooled.size(); i++) {
+ assertEquals(false, spooled.get(i), "embedded stream " + i + "
(" + lengths.get(i)
+ + " bytes) was spooled to disk to rewind instead of
re-opened");
+ }
+ }
+ }
+}