This is an automated email from the ASF dual-hosted git repository.

THausherr 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 b6103ab2e4 TIKA-4857: embedded-limits maxDepth counts embedding 
levels, not parser layers (#3098)
b6103ab2e4 is described below

commit b6103ab2e4a33c836c1e2eb1357e0c8d7c4f8fb7
Author: Dominik Schmidt <[email protected]>
AuthorDate: Mon Aug 31 13:29:19 2026 +0200

    TIKA-4857: embedded-limits maxDepth counts embedding levels, not parser 
layers (#3098)
    
    * TIKA-4857 - embedded-limits maxDepth counts embedding levels, not parser 
layers
    
    * TIKA-4857 - guard exitEmbedded against underflow; clarify the ordering 
comment
---
 CHANGES.txt                                        |   4 +
 .../ParsingEmbeddedDocumentExtractor.java          |  12 +-
 .../java/org/apache/tika/parser/ParseRecord.java   |  31 +++++
 .../tika/extractor/EmbeddedDepthLimitTest.java     | 130 +++++++++++++++++++++
 .../tika/pipes/core/extractor/UnpackExtractor.java |   6 +
 5 files changed, 180 insertions(+), 3 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index ea7a41d7d4..ce09fe0392 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,9 @@
 Release 4.1.0 - unreleased
 
+   * embedded-limits maxDepth counts embedding levels again instead of the
+     parsers a parse passes through; with AutoDetectParser over DefaultParser
+     every value above 1 used to stop one level early (TIKA-4857).
+
    * GeoGebraParser emits the icon of a tool (*.ggt, the macro's iconFile)
      as its THUMBNAIL embedded document; tool files have no thumbnail of
      their own (TIKA-4831).
diff --git 
a/tika-core/src/main/java/org/apache/tika/extractor/ParsingEmbeddedDocumentExtractor.java
 
b/tika-core/src/main/java/org/apache/tika/extractor/ParsingEmbeddedDocumentExtractor.java
index d8a585ffe6..c94383fe49 100644
--- 
a/tika-core/src/main/java/org/apache/tika/extractor/ParsingEmbeddedDocumentExtractor.java
+++ 
b/tika-core/src/main/java/org/apache/tika/extractor/ParsingEmbeddedDocumentExtractor.java
@@ -152,10 +152,10 @@ public class ParsingEmbeddedDocumentExtractor implements 
EmbeddedDocumentExtract
 
         // Depth limit only applies to current depth - siblings at shallower 
levels
         // can still be parsed. The flag is set for reporting purposes.
-        // depth is 1-indexed (main doc is depth 1), so embedded depth limit 
of N
-        // means we allow parsing up to depth N+1
+        // The child would be one level below the current document, so with
+        // maxDepth N the children of documents at depth N are not parsed.
         int maxDepth = parseRecord.getMaxEmbeddedDepth();
-        if (maxDepth >= 0 && parseRecord.getDepth() > maxDepth + 1) {
+        if (maxDepth >= 0 && parseRecord.getEmbeddedDepth() + 1 > maxDepth) {
             parseRecord.setEmbeddedDepthLimitReached(true);
             if (parseRecord.isThrowOnMaxDepth()) {
                 throw new EmbeddedLimitReachedException(
@@ -206,6 +206,9 @@ public class ParsingEmbeddedDocumentExtractor implements 
EmbeddedDocumentExtract
 
         // Use the delegate parser to parse this entry
         boolean parsedCleanly = false;
+        if (parseRecord != null) {
+            parseRecord.enterEmbedded();
+        }
         try {
             tis.setCloseShield();
             DELEGATING_PARSER.parse(tis,
@@ -221,6 +224,9 @@ public class ParsingEmbeddedDocumentExtractor implements 
EmbeddedDocumentExtract
         } catch (TikaException e) {
             recordException(e, context);
         } finally {
+            if (parseRecord != null) {
+                parseRecord.exitEmbedded();
+            }
             tis.removeCloseShield();
             if (outputHtml) {
                 // Only an aborted parse can leave elements open; on a clean 
parse
diff --git a/tika-core/src/main/java/org/apache/tika/parser/ParseRecord.java 
b/tika-core/src/main/java/org/apache/tika/parser/ParseRecord.java
index 01a8fd4cbf..88100d532e 100644
--- a/tika-core/src/main/java/org/apache/tika/parser/ParseRecord.java
+++ b/tika-core/src/main/java/org/apache/tika/parser/ParseRecord.java
@@ -59,6 +59,7 @@ public class ParseRecord {
 
     // Embedded document tracking
     private int embeddedCount = 0;
+    private int embeddedDepth = 0;
     private int maxEmbeddedDepth = -1;
     private int maxEmbeddedCount = -1;
     private boolean throwOnMaxDepth = false;
@@ -193,6 +194,36 @@ public class ParseRecord {
         embeddedCount++;
     }
 
+    /**
+     * Marks the start of an embedded document's parse: the documents parsed
+     * until {@link #exitEmbedded()} are one level deeper than the current one.
+     * Unlike {@link #getDepth()}, which counts the parsers a parse passes
+     * through, this counts embedding levels only.
+     */
+    public void enterEmbedded() {
+        embeddedDepth++;
+    }
+
+    /**
+     * Marks the end of an embedded document's parse.
+     */
+    public void exitEmbedded() {
+        if (embeddedDepth <= 0) {
+            throw new IllegalStateException("exitEmbedded() without a matching 
enterEmbedded()");
+        }
+        embeddedDepth--;
+    }
+
+    /**
+     * Gets the embedding depth of the document currently being parsed:
+     * 0 for the container, 1 for its embedded documents, and so on.
+     *
+     * @return the embedding depth of the current document
+     */
+    public int getEmbeddedDepth() {
+        return embeddedDepth;
+    }
+
     /**
      * Gets the current count of embedded documents processed.
      *
diff --git 
a/tika-core/src/test/java/org/apache/tika/extractor/EmbeddedDepthLimitTest.java 
b/tika-core/src/test/java/org/apache/tika/extractor/EmbeddedDepthLimitTest.java
new file mode 100644
index 0000000000..961ebb89ff
--- /dev/null
+++ 
b/tika-core/src/test/java/org/apache/tika/extractor/EmbeddedDepthLimitTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.extractor;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
+
+import org.apache.tika.config.EmbeddedLimits;
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.io.TikaInputStream;
+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.CompositeParser;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.parser.Parser;
+import org.apache.tika.parser.RecursiveParserWrapper;
+import org.apache.tika.sax.BasicContentHandlerFactory;
+import org.apache.tika.sax.RecursiveParserWrapperHandler;
+
+/**
+ * {@code maxDepth} of {@link EmbeddedLimits} counts embedding levels, not the
+ * parsers a parse passes through (TIKA-4857): with two composite layers per
+ * document, as {@code AutoDetectParser} over {@code DefaultParser} has, the
+ * limit used to stop one level early for every value above 1.
+ */
+public class EmbeddedDepthLimitTest {
+
+    private static final MediaType NESTED = MediaType.application("x-nested");
+
+    /**
+     * Every document of this type contains one document of the same type,
+     * five levels deep.
+     */
+    private static class NestingParser implements Parser {
+        @Override
+        public Set<MediaType> getSupportedTypes(ParseContext context) {
+            return Collections.singleton(NESTED);
+        }
+
+        @Override
+        public void parse(TikaInputStream stream, ContentHandler handler, 
Metadata metadata,
+                          ParseContext context) throws IOException, 
SAXException, TikaException {
+            int level = Integer.parseInt(new String(stream.readAllBytes(), 
StandardCharsets.UTF_8));
+            if (level >= 5) {
+                return;
+            }
+            Metadata child = new Metadata();
+            child.set(HttpHeaders.CONTENT_TYPE, NESTED.toString());
+            EmbeddedDocumentExtractor extractor =
+                    EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context);
+            if (extractor.shouldParseEmbedded(child, context)) {
+                try (TikaInputStream tis = TikaInputStream.get(
+                        String.valueOf(level + 
1).getBytes(StandardCharsets.UTF_8))) {
+                    extractor.parseEmbedded(tis, handler, child, context, 
false);
+                }
+            }
+        }
+    }
+
+    @ParameterizedTest
+    @CsvSource({"-1, 6, false", "0, 1, true", "1, 2, true", "2, 3, true", "3, 
4, true",
+            "5, 6, false"})
+    public void testMaxDepthCountsEmbeddingLevels(int maxDepth, int 
expectedDocuments,
+                                                  boolean limitReached) throws 
Exception {
+        //two composite layers, as AutoDetectParser over DefaultParser
+        Parser parser = new CompositeParser(new 
org.apache.tika.mime.MediaTypeRegistry(),
+                new CompositeParser(new 
org.apache.tika.mime.MediaTypeRegistry(),
+                        new NestingParser()));
+        RecursiveParserWrapper wrapper = new RecursiveParserWrapper(parser);
+        RecursiveParserWrapperHandler handler = new 
RecursiveParserWrapperHandler(
+                new 
BasicContentHandlerFactory(BasicContentHandlerFactory.HANDLER_TYPE.IGNORE,
+                        -1));
+        ParseContext context = new ParseContext();
+        EmbeddedLimits limits = new EmbeddedLimits();
+        limits.setMaxDepth(maxDepth);
+        context.set(EmbeddedLimits.class, limits);
+        Metadata metadata = new Metadata();
+        metadata.set(HttpHeaders.CONTENT_TYPE, NESTED.toString());
+
+        try (TikaInputStream tis = 
TikaInputStream.get("0".getBytes(StandardCharsets.UTF_8))) {
+            wrapper.parse(tis, handler, metadata, context);
+        }
+        List<Metadata> documents = handler.getMetadataList();
+        assertEquals(expectedDocuments, documents.size(), 
documents.toString());
+        //the container is first; embedded documents follow in the order they
+        //finish, so the deepest of them comes right after it
+        Set<Integer> depths = new HashSet<>();
+        for (Metadata document : documents) {
+            depths.add(document.getInt(TikaCoreProperties.EMBEDDED_DEPTH));
+        }
+        for (int depth = 0; depth < expectedDocuments; depth++) {
+            assertTrue(depths.contains(depth), "missing depth " + depth + " in 
" + depths);
+        }
+        if (limitReached) {
+            assertEquals("true",
+                    
documents.get(0).get(TikaCoreProperties.EMBEDDED_DEPTH_LIMIT_REACHED));
+        } else {
+            
assertNull(documents.get(0).get(TikaCoreProperties.EMBEDDED_DEPTH_LIMIT_REACHED));
+        }
+    }
+}
diff --git 
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/extractor/UnpackExtractor.java
 
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/extractor/UnpackExtractor.java
index daf4755fc9..6bc5be097a 100644
--- 
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/extractor/UnpackExtractor.java
+++ 
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/extractor/UnpackExtractor.java
@@ -100,6 +100,9 @@ public class UnpackExtractor extends 
ParsingEmbeddedDocumentExtractor {
         }
 
         // Use the delegate parser to parse this entry
+        if (parseRecord != null) {
+            parseRecord.enterEmbedded();
+        }
         try {
             tis.setCloseShield();
             UnpackHandler bytesHandler = context.get(UnpackHandler.class);
@@ -117,6 +120,9 @@ public class UnpackExtractor extends 
ParsingEmbeddedDocumentExtractor {
         } catch (TikaException e) {
             recordException(e, context);
         } finally {
+            if (parseRecord != null) {
+                parseRecord.exitEmbedded();
+            }
             tis.removeCloseShield();
         }
 

Reply via email to