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 4ead960bff TIKA-4879: carry on safer get missing part in xlsx and vsdx 
(#3135)
4ead960bff is described below

commit 4ead960bffffb7a83016e8f2d71c09e98359007b
Author: Tim Allison <[email protected]>
AuthorDate: Fri Sep 4 15:44:19 2026 -0400

    TIKA-4879: carry on safer get missing part in xlsx and vsdx (#3135)
---
 CHANGES.txt                                        |   7 +
 .../microsoft/ooxml/VSDXExtractorDecorator.java    |   4 +-
 .../ooxml/XSSFExcelExtractorDecorator.java         |   4 +-
 .../ooxml/OOXMLMissingRelatedPartTest.java         | 154 +++++++++++++++++++++
 4 files changed, 165 insertions(+), 4 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 10f3fc3ddc..64bfc36c74 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,12 @@
 Release 4.1.0 - unreleased
 
+   * A missing OOXML relationship target no longer aborts the whole file:
+     the threaded-comment and person lookups in xlsx and the page lookups in
+     vsdx went straight to POI's getRelatedPart, whose unchecked
+     IllegalArgumentException surfaced as "Error creating OOXML extractor" and
+     dropped the text already extracted. They route through
+     safeGetRelatedPart, as branch_3x already did (TIKA-4879).
+     
    * RawTiffDetector rejects a BigTIFF directory offset near Long.MAX_VALUE
      instead of letting the bounds check overflow. Adding the entry-count
      size to such an offset wrapped negative and read as "already in the
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/VSDXExtractorDecorator.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/VSDXExtractorDecorator.java
index 7fd0953c55..c8585f3138 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/VSDXExtractorDecorator.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/VSDXExtractorDecorator.java
@@ -90,7 +90,7 @@ public class VSDXExtractorDecorator extends 
AbstractOOXMLExtractor {
         PackageRelationshipCollection pageRels =
                 pagesPart.getRelationshipsByType(VISIO_PAGE_REL);
         for (PackageRelationship rel : pageRels) {
-            PackagePart pagePart = pagesPart.getRelatedPart(rel);
+            PackagePart pagePart = safeGetRelatedPart(pagesPart, rel);
             if (pagePart != null) {
                 pageParts.add(pagePart);
             }
@@ -113,7 +113,7 @@ public class VSDXExtractorDecorator extends 
AbstractOOXMLExtractor {
         if (rels.isEmpty()) {
             return null;
         }
-        return part.getRelatedPart(rels.getRelationship(0));
+        return safeGetRelatedPart(part, rels.getRelationship(0));
     }
 
     @Override
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/XSSFExcelExtractorDecorator.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFExcelExtractorDecorator.java
index 08d7c22f57..c7907f93e7 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFExcelExtractorDecorator.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFExcelExtractorDecorator.java
@@ -663,7 +663,7 @@ public class XSSFExcelExtractorDecorator extends 
AbstractOOXMLExtractor {
             return;
         }
         for (PackageRelationship rel : coll) {
-            PackagePart threadedCommentPart = sheetPart.getRelatedPart(rel);
+            PackagePart threadedCommentPart = safeGetRelatedPart(sheetPart, 
rel);
             if (threadedCommentPart == null) {
                 continue;
             }
@@ -690,7 +690,7 @@ public class XSSFExcelExtractorDecorator extends 
AbstractOOXMLExtractor {
             return;
         }
         for (PackageRelationship rel : coll) {
-            PackagePart personsPart = workbookPart.getRelatedPart(rel);
+            PackagePart personsPart = safeGetRelatedPart(workbookPart, rel);
             if (personsPart == null) {
                 continue;
             }
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ooxml/OOXMLMissingRelatedPartTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ooxml/OOXMLMissingRelatedPartTest.java
new file mode 100644
index 0000000000..1886234c2e
--- /dev/null
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ooxml/OOXMLMissingRelatedPartTest.java
@@ -0,0 +1,154 @@
+/*
+ * 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.ooxml;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
+import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
+import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
+import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.Test;
+
+import org.apache.tika.TikaTest;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.HttpHeaders;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
+
+/**
+ * An OOXML package may declare a relationship whose target part is missing -- 
a truncated
+ * or otherwise malformed file. POI's {@code PackagePart.getRelatedPart} then 
throws an
+ * unchecked {@code IllegalArgumentException}, which
+ * {@link OOXMLExtractorFactory} converts into a TikaException: the whole file 
aborts and
+ * even the text already extracted is lost. Every such call must go through
+ * {@link AbstractOOXMLExtractor#safeGetRelatedPart}.
+ *
+ * <p>Same failure class as the 3.3.2 docx regression (740 files crashed on a 
missing
+ * numbering.xml/settings.xml). 3.3.2 guards these four xlsx/vsdx sites; 4.0.0 
shipped
+ * without them (TIKA-4879).
+ */
+public class OOXMLMissingRelatedPartTest extends TikaTest {
+
+    private static final String XLSX_TYPE =
+            
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
+    private static final String VSDX_TYPE = "application/vnd.ms-visio.drawing";
+
+    @Test
+    public void testDanglingThreadedCommentRelationship() throws Exception {
+        //XSSFExcelExtractorDecorator.getThreadedComments
+        byte[] xlsx = withExtraRelationship("testComment.xlsx",
+                "xl/worksheets/_rels/sheet1.xml.rels",
+                
"http://schemas.microsoft.com/office/2017/10/relationships/threadedComment";,
+                "../threadedComments/threadedComment1.xml");
+        assertSheetTextSurvives(xlsx);
+    }
+
+    @Test
+    public void testDanglingPersonRelationship() throws Exception {
+        //XSSFExcelExtractorDecorator.getPersons
+        byte[] xlsx = withExtraRelationship("testComment.xlsx", 
"xl/_rels/workbook.xml.rels",
+                
"http://schemas.microsoft.com/office/2017/10/relationships/person";,
+                "persons/person.xml");
+        assertSheetTextSurvives(xlsx);
+    }
+
+    @Test
+    public void testMissingVisioPage() throws Exception {
+        //VSDXExtractorDecorator.getPageParts, the per-page loop
+        assertVisioParseCompletes(withoutEntry("testVISIO.vsdx", 
"visio/pages/page1.xml"));
+    }
+
+    @Test
+    public void testMissingVisioPagesPart() throws Exception {
+        //VSDXExtractorDecorator.getRelatedPart(PackagePart, String), 
document.xml -> pages.xml
+        assertVisioParseCompletes(withoutEntry("testVISIO.vsdx", 
"visio/pages/pages.xml"));
+    }
+
+    /**
+     * The pages are unreachable, but the parse must still run to completion: 
the EMF
+     * thumbnail is emitted after buildXHTML, so its presence proves we did 
not abort.
+     */
+    private void assertVisioParseCompletes(byte[] vsdx) throws Exception {
+        List<Metadata> metadataList = assertParses(vsdx, VSDX_TYPE);
+        assertEquals(2, metadataList.size());
+        assertEquals("image/emf", 
metadataList.get(1).get(HttpHeaders.CONTENT_TYPE));
+    }
+
+    /** The dangling relationship must not cost us the sheet text that parsed 
fine. */
+    private void assertSheetTextSurvives(byte[] xlsx) throws Exception {
+        List<Metadata> metadataList = assertParses(xlsx, XLSX_TYPE);
+        assertEquals(1, metadataList.size());
+        assertContains("Here is some text",
+                metadataList.get(0).get(TikaCoreProperties.TIKA_CONTENT));
+    }
+
+    private List<Metadata> assertParses(byte[] bytes, String expectedType) 
throws Exception {
+        List<Metadata> metadataList;
+        try (TikaInputStream tis = TikaInputStream.get(bytes)) {
+            //suppressException=false: an escaping IllegalArgumentException 
fails the test here
+            metadataList = getRecursiveMetadata(tis, false);
+        }
+        Metadata m = metadataList.get(0);
+        assertEquals(expectedType, m.get(HttpHeaders.CONTENT_TYPE));
+        assertNotNull(m.get(TikaCoreProperties.TIKA_CONTENT));
+        return metadataList;
+    }
+
+    /** Copies the resource, omitting one zip entry and leaving its 
relationship dangling. */
+    private byte[] withoutEntry(String resource, String entryName) throws 
Exception {
+        return copy(resource, entryName, null, null, null);
+    }
+
+    /** Copies the resource, appending a relationship whose target is not in 
the package. */
+    private byte[] withExtraRelationship(String resource, String relsEntry, 
String type,
+                                         String target) throws Exception {
+        return copy(resource, null, relsEntry, type, target);
+    }
+
+    private byte[] copy(String resource, String dropEntry, String relsEntry, 
String type,
+                        String target) throws Exception {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        try (ZipArchiveInputStream zin =
+                     new 
ZipArchiveInputStream(getResourceAsStream("/test-documents/" + resource));
+             ZipArchiveOutputStream zout = new ZipArchiveOutputStream(bos)) {
+            ZipArchiveEntry entry;
+            while ((entry = zin.getNextEntry()) != null) {
+                if (entry.getName().equals(dropEntry)) {
+                    continue;
+                }
+                byte[] data = IOUtils.toByteArray(zin);
+                if (entry.getName().equals(relsEntry)) {
+                    String rels = new String(data, StandardCharsets.UTF_8);
+                    String injected = "<Relationship Id=\"rIdMissingTarget\" 
Type=\"" + type +
+                            "\" Target=\"" + target + "\"/></Relationships>";
+                    data = rels.replace("</Relationships>", injected)
+                            .getBytes(StandardCharsets.UTF_8);
+                }
+                zout.putArchiveEntry(new ZipArchiveEntry(entry.getName()));
+                zout.write(data);
+                zout.closeArchiveEntry();
+            }
+        }
+        return bos.toByteArray();
+    }
+}

Reply via email to