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 4a563d6fdb Add a placeholder marker (#3122)
4a563d6fdb is described below

commit 4a563d6fdb5248f3fd0b259043a610c6fa3ef2b0
Author: Tim Allison <[email protected]>
AuthorDate: Thu Sep 3 14:32:32 2026 -0400

    Add a placeholder marker (#3122)
---
 CHANGES.txt                                        |   5 +
 .../java/org/apache/tika/io/PlaceholderSource.java | 122 +++++++++++++++++++++
 .../java/org/apache/tika/io/TikaInputSource.java   |   8 ++
 .../java/org/apache/tika/io/TikaInputStream.java   |  14 ++-
 .../org/apache/tika/renderer/RenderResult.java     |   3 +-
 .../org/apache/tika/io/PlaceholderStreamTest.java  |  63 +++++++++++
 .../tika/parser/microsoft/MetafileRendering.java   |   2 +-
 .../apache/tika/parser/microsoft/OfficeParser.java |  11 +-
 .../apache/tika/parser/pdf/AbstractPDF2XHTML.java  |   2 +-
 .../tika/parser/pdf/image/ImageGraphicsEngine.java |   2 +-
 10 files changed, 224 insertions(+), 8 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 7a5ac09278..e6714ec615 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,10 @@
 Release 4.1.0 - unreleased
 
+   * Placeholder streams -- the empty stand-ins parsers hand parseEmbedded
+     for content that is never extracted -- report an unknown length rather
+     than their own zero, and the macro-failure entry is registered without
+     parsing its sentinel (TIKA-4874).
+
    * TikaInputStream.hasReliableLength() distinguishes measured lengths
      from declared Content-Length hints, and one-shot streams now carry a
      declared length without spooling; detection sizes its magic read only
diff --git a/tika-core/src/main/java/org/apache/tika/io/PlaceholderSource.java 
b/tika-core/src/main/java/org/apache/tika/io/PlaceholderSource.java
new file mode 100644
index 0000000000..e03a44dff0
--- /dev/null
+++ b/tika-core/src/main/java/org/apache/tika/io/PlaceholderSource.java
@@ -0,0 +1,122 @@
+/*
+ * 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.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.Path;
+
+/**
+ * Empty stand-in for content that is never extracted: its emptiness describes 
the
+ * placeholder, not the document, so it reports an unknown length rather than 
zero.
+ *
+ * @see TikaInputStream#getPlaceholder()
+ */
+class PlaceholderSource extends InputStream implements TikaInputSource {
+
+    private final TemporaryResources tmp;
+    private Path spilledPath;
+
+    PlaceholderSource(TemporaryResources tmp) {
+        this.tmp = tmp;
+    }
+
+    @Override
+    public int read() {
+        return -1;
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) {
+        // InputStream contract: a zero-length read returns 0, even at EOF
+        return len == 0 ? 0 : -1;
+    }
+
+    @Override
+    public long skip(long n) {
+        return 0;
+    }
+
+    @Override
+    public int available() {
+        return 0;
+    }
+
+    @Override
+    public void seekTo(long newPosition) throws IOException {
+        if (newPosition != 0) {
+            throw new IOException("Invalid seek position: " + newPosition + " 
(empty source)");
+        }
+    }
+
+    @Override
+    public Path materializedPath() {
+        return spilledPath;
+    }
+
+    @Override
+    public boolean hasPath() {
+        return spilledPath != null;
+    }
+
+    @Override
+    public Path getPath(String suffix) throws IOException {
+        if (spilledPath == null) {
+            spilledPath = tmp.createTempFile(suffix);
+        }
+        return spilledPath;
+    }
+
+    @Override
+    public long getLength() {
+        return -1;
+    }
+
+    @Override
+    public boolean hasReliableLength() {
+        return false;
+    }
+
+    @Override
+    public boolean isPlaceholder() {
+        return true;
+    }
+
+    @Override
+    public void enableRewind(CacheMemoryBudget budget) {
+        // No-op: there is nothing to rewind
+    }
+
+    @Override
+    public SeekableByteChannel getSeekableByteChannel() {
+        return new MemorySeekableByteChannel(new byte[0], 0);
+    }
+
+    @Override
+    public synchronized void mark(int readlimit) {
+    }
+
+    @Override
+    public synchronized void reset() {
+    }
+
+    @Override
+    public boolean markSupported() {
+        return true;
+    }
+}
diff --git a/tika-core/src/main/java/org/apache/tika/io/TikaInputSource.java 
b/tika-core/src/main/java/org/apache/tika/io/TikaInputSource.java
index 9f3dc96146..380db7a638 100644
--- a/tika-core/src/main/java/org/apache/tika/io/TikaInputSource.java
+++ b/tika-core/src/main/java/org/apache/tika/io/TikaInputSource.java
@@ -69,6 +69,14 @@ interface TikaInputSource extends Closeable {
      */
     boolean hasReliableLength();
 
+    /**
+     * Whether this source stands in for content that is never extracted. 
Spooling one
+     * measures nothing, so its unknown length must not cost a temp file to 
confirm.
+     */
+    default boolean isPlaceholder() {
+        return false;
+    }
+
     /**
      * Enables full rewind capability.
      * <p>
diff --git a/tika-core/src/main/java/org/apache/tika/io/TikaInputStream.java 
b/tika-core/src/main/java/org/apache/tika/io/TikaInputStream.java
index ae62397b9e..398bf53778 100644
--- a/tika-core/src/main/java/org/apache/tika/io/TikaInputStream.java
+++ b/tika-core/src/main/java/org/apache/tika/io/TikaInputStream.java
@@ -149,6 +149,18 @@ public class TikaInputStream extends TaggedInputStream {
         return new TikaInputStream(inputSource, tmp, ext);
     }
 
+    /**
+     * An empty stream standing in for content that is never extracted -- a 
metadata-only
+     * entry, a rendering carried as an open container. It reports an 
<em>unknown</em>
+     * length, so nothing mistakes the placeholder's size for the document's. 
Pair it with
+     * {@link org.apache.tika.parser.MetadataOnlyParse} to register an entry 
without
+     * parsing it, unless an open container supplies the content.
+     */
+    public static TikaInputStream getPlaceholder() {
+        TemporaryResources tmp = new TemporaryResources();
+        return new TikaInputStream(new PlaceholderSource(tmp), tmp, "");
+    }
+
     public static TikaInputStream get(Path path) throws IOException {
         return get(path, new Metadata());
     }
@@ -473,7 +485,7 @@ public class TikaInputStream extends TaggedInputStream {
             return -1;
         }
         long len = source.getLength();
-        if (len == -1) {
+        if (len == -1 && !source.isPlaceholder()) {
             // Force spill to get length
             getPath();
             len = source.getLength();
diff --git a/tika-core/src/main/java/org/apache/tika/renderer/RenderResult.java 
b/tika-core/src/main/java/org/apache/tika/renderer/RenderResult.java
index 25588c45bb..db6ab42bdf 100644
--- a/tika-core/src/main/java/org/apache/tika/renderer/RenderResult.java
+++ b/tika-core/src/main/java/org/apache/tika/renderer/RenderResult.java
@@ -65,7 +65,8 @@ public class RenderResult implements Closeable {
         if (result instanceof Path) {
             return TikaInputStream.get((Path)result, metadata);
         } else {
-            TikaInputStream tis = TikaInputStream.get(new byte[0]);
+            // the rendering rides in the open container, not the stream
+            TikaInputStream tis = TikaInputStream.getPlaceholder();
             tis.setOpenContainer(result);
             return tis;
         }
diff --git 
a/tika-core/src/test/java/org/apache/tika/io/PlaceholderStreamTest.java 
b/tika-core/src/test/java/org/apache/tika/io/PlaceholderStreamTest.java
new file mode 100644
index 0000000000..8dd2bffbe8
--- /dev/null
+++ b/tika-core/src/test/java/org/apache/tika/io/PlaceholderStreamTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.io;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.file.Files;
+
+import org.junit.jupiter.api.Test;
+
+public class PlaceholderStreamTest {
+
+    @Test
+    public void testPlaceholderDeclaresNoLength() throws Exception {
+        try (TikaInputStream tis = TikaInputStream.getPlaceholder()) {
+            assertFalse(tis.hasLength(), "a placeholder's size describes 
nothing");
+            assertEquals(-1, tis.read(), "placeholder is empty");
+        }
+    }
+
+    /** A real empty document is not a placeholder: zero is honest there. */
+    @Test
+    public void testGenuinelyEmptyStreamStillDeclaresZero() throws Exception {
+        try (TikaInputStream tis = TikaInputStream.get(new byte[0])) {
+            assertTrue(tis.hasLength());
+            assertEquals(0, tis.getLength());
+        }
+    }
+
+    /** Spooling must not turn the placeholder's absent length into a zero. */
+    @Test
+    public void testSpoolingKeepsLengthUnknown() throws Exception {
+        try (TikaInputStream tis = TikaInputStream.getPlaceholder()) {
+            assertEquals(0, Files.size(tis.getPath()));
+            assertFalse(tis.hasLength());
+        }
+    }
+
+    /** Measuring a placeholder must not cost a temp file: there is nothing to 
measure. */
+    @Test
+    public void testMeasuringCostsNoTempFile() throws Exception {
+        try (TikaInputStream tis = TikaInputStream.getPlaceholder()) {
+            assertEquals(-1, tis.getLength());
+            assertFalse(tis.hasFile());
+        }
+    }
+}
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileRendering.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileRendering.java
index d4efd0a12c..0032c97a64 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileRendering.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileRendering.java
@@ -119,7 +119,7 @@ final class MetafileRendering {
         if (source != null && source.hasFile()) {
             return TikaInputStream.get(source.getPath());
         }
-        return TikaInputStream.get(new byte[0]);
+        return TikaInputStream.getPlaceholder();
     }
 
     /**
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/OfficeParser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/OfficeParser.java
index 423a51f86b..925e688f58 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/OfficeParser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/OfficeParser.java
@@ -55,6 +55,7 @@ import org.apache.tika.metadata.HttpHeaders;
 import org.apache.tika.metadata.Metadata;
 import org.apache.tika.metadata.TikaCoreProperties;
 import org.apache.tika.mime.MediaType;
+import org.apache.tika.parser.MetadataOnlyParse;
 import org.apache.tika.parser.ParseContext;
 import org.apache.tika.parser.PasswordProvider;
 import org.apache.tika.parser.microsoft.ooxml.OOXMLParser;
@@ -130,9 +131,13 @@ public class OfficeParser extends AbstractOfficeParser {
             m.set(HttpHeaders.CONTENT_TYPE, "text/x-vbasic");
             EmbeddedDocumentUtil.recordException(e, m, context);
             if (embeddedDocumentExtractor.shouldParseEmbedded(m, context)) {
-                embeddedDocumentExtractor.parseEmbedded(
-                        //pass in space character so that we don't trigger a 
zero-byte exception
-                        TikaInputStream.get(new byte[]{'\u0020'}), xhtml, m, 
context, true);
+                // the entry carries the exception, not content: register it 
without a parse
+                try (TikaInputStream tis = TikaInputStream.getPlaceholder()) {
+                    context.set(MetadataOnlyParse.class, 
MetadataOnlyParse.INSTANCE);
+                    embeddedDocumentExtractor.parseEmbedded(tis, xhtml, m, 
context, true);
+                } finally {
+                    context.set(MetadataOnlyParse.class, null);
+                }
             }
             return;
         }
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/AbstractPDF2XHTML.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/AbstractPDF2XHTML.java
index aa57face74..da4a51ed43 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/AbstractPDF2XHTML.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/AbstractPDF2XHTML.java
@@ -640,7 +640,7 @@ class AbstractPDF2XHTML extends PDFTextStripper {
                     new PageRangeRequest(getCurrentPageNo(), 
getCurrentPageNo());
             if (thisRenderer instanceof PDDocumentRenderer) {
                 //do not do autocloseable.  We need to leave the pdDocument 
open!
-                TikaInputStream tis = TikaInputStream.get(new byte[0]);
+                TikaInputStream tis = TikaInputStream.getPlaceholder();
                 tis.setOpenContainer(pdDocument);
                 return thisRenderer.render(tis, pageMetadata, context, 
pageRangeRequest)
                         .getResults().get(0);
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/image/ImageGraphicsEngine.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/image/ImageGraphicsEngine.java
index 1c04c23a44..e4387802f9 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/image/ImageGraphicsEngine.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/image/ImageGraphicsEngine.java
@@ -451,7 +451,7 @@ public class ImageGraphicsEngine extends 
PDFGraphicsStreamEngine {
         metadata.set(TIFF.IMAGE_LENGTH, pdImage.getHeight());
         //TODO: what else can we extract from the PDImage without rendering?
         //Register the image's metadata entry without decoding it (marker 
skips the parse).
-        try (TikaInputStream tis = TikaInputStream.get(new byte[0])) {
+        try (TikaInputStream tis = TikaInputStream.getPlaceholder()) {
             parseContext.set(MetadataOnlyParse.class, 
MetadataOnlyParse.INSTANCE);
             embeddedDocumentExtractor.parseEmbedded(tis,
                     new EmbeddedContentHandler(xhtml), metadata, parseContext, 
false);

Reply via email to