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();
}