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");
+            }
+        }
+    }
+}

Reply via email to