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

tballison pushed a commit to branch xssf-vsdx-safe-getrelatedpart
in repository https://gitbox.apache.org/repos/asf/tika.git

commit dae34a4e22b4006096a9802ebcbd1a4c60f635d2
Author: tallison <[email protected]>
AuthorDate: Mon Jul 13 13:43:06 2026 -0400

    Improve SAX/ooxml parsing
---
 .../tika/parser/microsoft/AbstractListManager.java |   6 +
 .../microsoft/ooxml/TikaSheetXMLHandler.java       |  28 ++-
 .../ooxml/TikaXSSFBSharedStringsTable.java         |   8 +
 .../microsoft/ooxml/VSDXExtractorDecorator.java    |   4 +-
 .../ooxml/XSSFBExcelExtractorDecorator.java        |  57 ++++++-
 .../ooxml/XSSFExcelExtractorDecorator.java         |  16 +-
 .../microsoft/ooxml/XSSFSharedStringsShim.java     |   7 +
 .../microsoft/ooxml/xwpf/XWPFNumberingShim.java    |   6 +-
 .../microsoft/ParagraphLevelCounterTest.java       |  47 +++++
 .../ooxml/OOXMLMissingRelatedPartTest.java         | 190 +++++++++++++++++++++
 .../ooxml/SXWPFMissingRelatedPartTest.java         | 101 +++++++++++
 .../ooxml/SXWPFNonStandardNumberingTest.java       |  87 ++++++++++
 .../parser/microsoft/ooxml/XSSFBMalformedTest.java |  87 ++++++++++
 .../microsoft/ooxml/XSSFMalformedSheetTest.java    | 101 +++++++++++
 14 files changed, 723 insertions(+), 22 deletions(-)

diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/AbstractListManager.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/AbstractListManager.java
index 12d806d09d..eef5a25e89 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/AbstractListManager.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/AbstractListManager.java
@@ -60,6 +60,12 @@ public abstract class AbstractListManager {
          */
         public String incrementLevel(int levelNumber, LevelTuple[] 
overrideLevelTuples) {
 
+            if (levelNumber < 0 || levelNumber >= levelTuples.length) {
+                // Out-of-range level: format() returns "" anyway; return 
early so a huge
+                // levelNumber can't grow `counts` into an OOM.
+                return "";
+            }
+
             for (int i = lastLevel + 1; i < levelNumber; i++) {
                 if (i >= counts.size()) {
                     int val = getStart(i, overrideLevelTuples);
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/TikaSheetXMLHandler.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/TikaSheetXMLHandler.java
index e95506be5d..87b169e785 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/TikaSheetXMLHandler.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/TikaSheetXMLHandler.java
@@ -160,7 +160,12 @@ class TikaSheetXMLHandler extends DefaultHandler {
         } else if ("row".equals(localName)) {
             String rowNumStr = attributes.getValue("r");
             if (rowNumStr != null) {
-                rowNum = Integer.parseInt(rowNumStr.trim()) - 1;
+                try {
+                    rowNum = Integer.parseInt(rowNumStr.trim()) - 1;
+                } catch (NumberFormatException e) {
+                    //non-numeric r attribute; fall back to positional 
numbering
+                    rowNum = nextRowNum;
+                }
             } else {
                 rowNum = nextRowNum;
             }
@@ -190,7 +195,12 @@ class TikaSheetXMLHandler extends DefaultHandler {
                 if (stylesShim != null) {
                     int styleIndex;
                     if (cellStyleStr != null) {
-                        styleIndex = Integer.parseInt(cellStyleStr.trim());
+                        try {
+                            styleIndex = Integer.parseInt(cellStyleStr.trim());
+                        } catch (NumberFormatException e) {
+                            //non-numeric s attribute; skip style resolution
+                            styleIndex = -1;
+                        }
                     } else if (stylesShim.getNumCellStyles() > 0) {
                         styleIndex = 0;
                     } else {
@@ -267,8 +277,8 @@ class TikaSheetXMLHandler extends DefaultHandler {
         } else {
             switch (nextDataType) {
                 case BOOLEAN:
-                    char first = value.charAt(0);
-                    thisStr = first == '0' ? "FALSE" : "TRUE";
+                    //empty <v> is malformed; treat as FALSE rather than 
charAt(0) an empty string
+                    thisStr = value.length() > 0 && value.charAt(0) != '0' ? 
"TRUE" : "FALSE";
                     break;
                 case ERROR:
                     thisStr = "ERROR:" + value;
@@ -326,8 +336,14 @@ class TikaSheetXMLHandler extends DefaultHandler {
         }
 
         checkForEmptyCellComments(EmptyCellCommentsCheckType.CELL);
-        XSSFCommentsShim.CommentData comment = commentsShim != null ?
-                commentsShim.findCellComment(new CellAddress(cellRef)) : null;
+        XSSFCommentsShim.CommentData comment = null;
+        if (commentsShim != null && cellRef != null) {
+            try {
+                comment = commentsShim.findCellComment(new 
CellAddress(cellRef));
+            } catch (IllegalArgumentException e) {
+                //malformed cell ref; skip the comment lookup
+            }
+        }
         output.cell(cellRef, thisStr, comment);
     }
 
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/TikaXSSFBSharedStringsTable.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/TikaXSSFBSharedStringsTable.java
index 6e1ec853d5..de67428f58 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/TikaXSSFBSharedStringsTable.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/TikaXSSFBSharedStringsTable.java
@@ -48,6 +48,10 @@ class TikaXSSFBSharedStringsTable implements SharedStrings {
     private int uniqueCount;
     private final List<String> strings = new ArrayList<>();
 
+    // Empty table -- fallback when sharedStrings.bin is corrupt/unreadable.
+    TikaXSSFBSharedStringsTable() {
+    }
+
     TikaXSSFBSharedStringsTable(OPCPackage pkg) throws IOException {
         ArrayList<PackagePart> parts =
                 pkg.getPartsByContentType(SHARED_STRINGS_BINARY_CT);
@@ -65,6 +69,10 @@ class TikaXSSFBSharedStringsTable implements SharedStrings {
 
     @Override
     public RichTextString getItemAt(int idx) {
+        if (idx < 0 || idx >= strings.size()) {
+            //malformed workbook can reference a string the table doesn't have
+            return new PlainRichTextString("");
+        }
         return new PlainRichTextString(strings.get(idx));
     }
 
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 7e671057c2..8fbe7833a6 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
@@ -93,7 +93,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);
             }
@@ -116,7 +116,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/XSSFBExcelExtractorDecorator.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFBExcelExtractorDecorator.java
index d59ddd6528..2d653bf441 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFBExcelExtractorDecorator.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFBExcelExtractorDecorator.java
@@ -35,11 +35,13 @@ import org.xml.sax.ContentHandler;
 import org.xml.sax.SAXException;
 
 import org.apache.tika.exception.TikaException;
+import org.apache.tika.exception.WriteLimitReachedException;
 import org.apache.tika.metadata.Metadata;
 import org.apache.tika.metadata.Office;
 import org.apache.tika.metadata.TikaCoreProperties;
 import org.apache.tika.parser.ParseContext;
 import org.apache.tika.sax.XHTMLContentHandler;
+import org.apache.tika.utils.ExceptionUtils;
 
 public class XSSFBExcelExtractorDecorator extends XSSFExcelExtractorDecorator {
 
@@ -76,14 +78,41 @@ public class XSSFBExcelExtractorDecorator extends 
XSSFExcelExtractorDecorator {
             }
             styles = xssfReader.getXSSFBStylesTable();
             iter = (XSSFBReader.SheetIterator) xssfReader.getSheetsData();
-            strings = new TikaXSSFBSharedStringsTable(container);
         } catch (OpenXML4JException e) {
             throw new IOException(e);
         }
+        // Shared strings are optional -- a corrupt/truncated 
sharedStrings.bin degrades to an
+        // empty table (getItemAt then returns "") rather than aborting the 
workbook.
+        try {
+            strings = new TikaXSSFBSharedStringsTable(container);
+        } catch (IOException | RuntimeException e) {
+            metadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
+                    ExceptionUtils.getStackTrace(e));
+            strings = new TikaXSSFBSharedStringsTable();
+        }
 
-        while (iter.hasNext()) {
-            InputStream stream = iter.next();
-            PackagePart sheetPart = iter.getSheetPart();
+        while (true) {
+            try {
+                if (!iter.hasNext()) {
+                    break;
+                }
+            } catch (RuntimeException e) {
+                metadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
+                        ExceptionUtils.getStackTrace(e));
+                break;
+            }
+            InputStream stream;
+            PackagePart sheetPart;
+            try {
+                stream = iter.next();
+                sheetPart = iter.getSheetPart();
+            } catch (RuntimeException e) {
+                // POI throws POIXMLException when a referenced sheet part is 
missing; its
+                // iterator state may not advance, so break rather than risk 
an infinite loop.
+                metadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
+                        ExceptionUtils.getStackTrace(e));
+                break;
+            }
             addDrawingHyperLinks(sheetPart);
             sheetParts.add(sheetPart);
 
@@ -103,10 +132,19 @@ public class XSSFBExcelExtractorDecorator extends 
XSSFExcelExtractorDecorator {
 
             // Pass null for POI's comments table to avoid xmlbeans dependency.
             // Comments are emitted separately after sheet processing.
-            XSSFBSheetHandler xssfbSheetHandler =
-                    new XSSFBSheetHandler(stream, styles, null, strings,
-                            sheetExtractor, formatter, false);
-            xssfbSheetHandler.parse();
+            try (InputStream sheetStream = stream) {
+                XSSFBSheetHandler xssfbSheetHandler =
+                        new XSSFBSheetHandler(sheetStream, styles, null, 
strings,
+                                sheetExtractor, formatter, false);
+                xssfbSheetHandler.parse();
+            } catch (RuntimeException | IOException e) {
+                // Truncated/malformed binary sheet -- keep prior sheets and 
close any open
+                // row/cell. A wrapped write-limit signal must still propagate.
+                WriteLimitReachedException.throwIfWriteLimitReached(e);
+                metadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
+                        ExceptionUtils.getStackTrace(e));
+                sheetExtractor.closeAnyPending();
+            }
 
             xhtml.endElement("tbody");
             xhtml.endElement("table");
@@ -149,7 +187,8 @@ public class XSSFBExcelExtractorDecorator extends 
XSSFExcelExtractorDecorator {
             try (InputStream is = commentsPart.getInputStream()) {
                 return new TikaXSSFBCommentsTable(is);
             }
-        } catch (InvalidFormatException | IOException e) {
+        } catch (InvalidFormatException | IOException | RuntimeException e) {
+            //missing/corrupt comments part must not abort the sheet
             return null;
         }
     }
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 111b94b578..887934b472 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
@@ -687,7 +687,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;
             }
@@ -714,7 +714,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;
             }
@@ -1073,8 +1073,16 @@ public class XSSFExcelExtractorDecorator extends 
AbstractOOXMLExtractor {
                           XSSFCommentsShim.CommentData comment) {
             try {
                 // Handle any missing cells
-                int colNum =
-                        (cellRef == null) ? lastSeenCol + 1 : (new 
CellReference(cellRef)).getCol();
+                int colNum = lastSeenCol + 1;
+                if (cellRef != null) {
+                    try {
+                        colNum = new CellReference(cellRef).getCol();
+                    } catch (IllegalArgumentException e) {
+                        //malformed cell ref (also covers 
NumberFormatException on an
+                        //overflowing row); treat like a cell with no ref
+                        colNum = lastSeenCol + 1;
+                    }
+                }
                 for (int cn = lastSeenCol + 1; cn < colNum; cn++) {
                     xhtml.startElement("td");
                     cellOpen = 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/XSSFSharedStringsShim.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFSharedStringsShim.java
index 8556d0fbb3..89be221272 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFSharedStringsShim.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFSharedStringsShim.java
@@ -59,7 +59,14 @@ class XSSFSharedStringsShim {
         this.strings = handler.strings;
     }
 
+    /**
+     * @return the shared string at {@code idx}, or null if out of range -- a 
malformed
+     * workbook can reference a string the table doesn't have; that must not 
abort the parse.
+     */
     String getItemAt(int idx) {
+        if (idx < 0 || idx >= strings.size()) {
+            return null;
+        }
         return strings.get(idx);
     }
 
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/xwpf/XWPFNumberingShim.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/xwpf/XWPFNumberingShim.java
index 7d8d15fc7f..ed7c8d6d8d 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/xwpf/XWPFNumberingShim.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/xwpf/XWPFNumberingShim.java
@@ -43,6 +43,10 @@ public class XWPFNumberingShim {
 
     public static final XWPFNumberingShim EMPTY = new EmptyNumberingShim();
 
+    // OOXML defines 9 list levels (w:ilvl 0-8); ignore anything beyond that 
so a huge
+    // ilvl can't grow the dense level array in endElement("abstractNum") into 
an OOM.
+    private static final int MAX_NUMBERING_LEVELS = 9;
+
     // abstractNumId -> list of LevelTuples (indexed by ilvl)
     private final Map<Integer, LevelTuple[]> abstractNumLevels = new 
HashMap<>();
     // numId -> abstractNumId
@@ -217,7 +221,7 @@ public class XWPFNumberingShim {
             }
             switch (localName) {
                 case "lvl":
-                    if (inLvl && currentIlvl >= 0) {
+                    if (inLvl && currentIlvl >= 0 && currentIlvl < 
MAX_NUMBERING_LEVELS) {
                         LevelTuple tuple = buildLevelTuple(currentIlvl);
                         if (inLvlOverride && inNum) {
                             currentOverrides.put(currentIlvl, tuple);
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ParagraphLevelCounterTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ParagraphLevelCounterTest.java
new file mode 100644
index 0000000000..d9e5b7f69f
--- /dev/null
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ParagraphLevelCounterTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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 org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+import org.apache.tika.parser.microsoft.AbstractListManager.LevelTuple;
+import 
org.apache.tika.parser.microsoft.AbstractListManager.ParagraphLevelCounter;
+
+public class ParagraphLevelCounterTest {
+
+    // A paragraph ilvl beyond the defined levels must yield "" instead of 
growing `counts`
+    // to that many entries (a large value would OOM). The timeout guards the 
unbounded loop.
+    @Test
+    @Timeout(10)
+    public void testOutOfRangeLevelReturnsEmpty() {
+        LevelTuple[] levels = {new LevelTuple("%1.")};
+        ParagraphLevelCounter counter = new ParagraphLevelCounter(levels);
+        assertEquals("", counter.incrementLevel(100_000_000, null));
+        assertEquals("", counter.incrementLevel(-1, null));
+    }
+
+    // A normal in-range level still formats.
+    @Test
+    public void testInRangeLevelStillFormats() {
+        LevelTuple[] levels = {new LevelTuple(1, -1, "%1.", "decimal", false)};
+        ParagraphLevelCounter counter = new ParagraphLevelCounter(levels);
+        assertEquals("1. ", counter.incrementLevel(0, null));
+    }
+}
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..73330fc6ec
--- /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,190 @@
+/*
+ * 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.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+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.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.parser.ParseContext;
+
+/**
+ * An OOXML file that declares a relationship to a part whose target is 
missing makes POI's
+ * getRelatedPart throw an unchecked IllegalArgumentException. Extractors must 
skip it, not
+ * abort the whole parse. Companion to {@link SXWPFMissingRelatedPartTest} 
(docx path); this
+ * covers the xlsx (threaded comments, persons) and vsdx paths.
+ */
+public class OOXMLMissingRelatedPartTest extends TikaTest {
+
+    private static final String THREADED_COMMENT_REL =
+            
"http://schemas.microsoft.com/office/2017/10/relationships/threadedComment";;
+    private static final String PERSON_REL =
+            "http://schemas.microsoft.com/office/2017/10/relationships/person";;
+
+    private static final String RELS_NS =
+            "http://schemas.openxmlformats.org/package/2006/relationships";;
+
+    private static final String RELS_CLOSE = "</Relationships>";
+
+    @Test
+    public void testXLSXMissingPersonsPart() throws Exception {
+        //person rel whose target is not in the package
+        String rel = "<Relationship Id=\"rIdPersons\" Type=\"" + PERSON_REL +
+                "\" Target=\"persons/person.xml\"/>";
+
+        Metadata m = withDanglingRel("testEXCEL.xlsx", 
"xl/_rels/workbook.xml.rels", rel);
+        assertContains("Sample Excel Worksheet", 
m.get(TikaCoreProperties.TIKA_CONTENT));
+    }
+
+    @Test
+    public void testXLSXMissingThreadedCommentsPart() throws Exception {
+        //sheet threadedComment rel whose target is not in the package
+        String rel = "<Relationship Id=\"rIdTc\" Type=\"" + 
THREADED_COMMENT_REL +
+                "\" Target=\"../threadedComments/threadedComment1.xml\"/>";
+
+        Metadata m = withDanglingRel("testEXCEL.xlsx", 
"xl/worksheets/_rels/sheet1.xml.rels", rel);
+        assertContains("Sample Excel Worksheet", 
m.get(TikaCoreProperties.TIKA_CONTENT));
+    }
+
+    /**
+     * Not a getRelatedPart case: a workbook with no sharedStrings part or 
relationship whose
+     * cells still carry t="s" indices. Every index is out of range for the 
empty table -- must
+     * degrade to empty cells, not an IndexOutOfBoundsException.
+     */
+    @Test
+    public void testXLSXMissingSharedStringsPart() throws Exception {
+        byte[] xlsx = mutate("testEXCEL.xlsx", Map.of(), 
Set.of("xl/sharedStrings.xml"));
+        //drop the rel too, so POI yields an empty table instead of throwing
+        xlsx = stripSharedStringsRel(xlsx);
+
+        Metadata m = parse(xlsx, "testEXCEL.xlsx");
+        //numeric cells survive though every shared string is unresolvable
+        assertContains("<td>144</td>", m.get(TikaCoreProperties.TIKA_CONTENT));
+    }
+
+    @Test
+    public void testVSDXMissingPagePart() throws Exception {
+        //drop page1.xml, leaving the dangling relationship in pages.xml.rels
+        Metadata m = parseMutated("testVISIO_text.vsdx", Map.of(),
+                Set.of("visio/pages/page1.xml"));
+        assertNotNull(m.get(TikaCoreProperties.TIKA_CONTENT));
+    }
+
+    @Test
+    public void testVSDXMissingPagesPart() throws Exception {
+        //drop pages.xml, leaving the dangling relationship in 
document.xml.rels
+        Metadata m = parseMutated("testVISIO_text.vsdx", Map.of(),
+                Set.of("visio/pages/pages.xml"));
+        assertNotNull(m.get(TikaCoreProperties.TIKA_CONTENT));
+    }
+
+    /** Appends relXml to relsEntry (creating it if absent) and parses; the 
target is missing. */
+    private Metadata withDanglingRel(String resource, String relsEntry, String 
relXml)
+            throws Exception {
+        return parseMutated(resource, Map.of(relsEntry, relXml), Set.of());
+    }
+
+    private Metadata parseMutated(String resource, Map<String, String> 
relAppends,
+                                  Set<String> toDrop) throws Exception {
+        return parse(mutate(resource, relAppends, toDrop), resource);
+    }
+
+    private Metadata parse(byte[] bytes, String desc) throws Exception {
+        List<Metadata> metadataList;
+        try (TikaInputStream tis = TikaInputStream.get(bytes)) {
+            metadataList = getRecursiveMetadata(tis, new Metadata(), new 
ParseContext(), false);
+        }
+        assertEquals(1, metadataList.size(), desc);
+        return metadataList.get(0);
+    }
+
+    /** Removes the sharedStrings relationship from 
xl/_rels/workbook.xml.rels. */
+    private byte[] stripSharedStringsRel(byte[] xlsx) throws Exception {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        try (ZipArchiveInputStream zin = new ZipArchiveInputStream(new 
ByteArrayInputStream(xlsx));
+             ZipArchiveOutputStream zout = new ZipArchiveOutputStream(bos)) {
+            ZipArchiveEntry entry;
+            while ((entry = zin.getNextEntry()) != null) {
+                zout.putArchiveEntry(new ZipArchiveEntry(entry.getName()));
+                if ("xl/_rels/workbook.xml.rels".equals(entry.getName())) {
+                    String rels = new String(IOUtils.toByteArray(zin), 
StandardCharsets.UTF_8);
+                    
zout.write(rels.replaceAll("<Relationship[^>]*sharedStrings\\.xml\"\\s*/>", "")
+                            .getBytes(StandardCharsets.UTF_8));
+                } else {
+                    IOUtils.copy(zin, zout);
+                }
+                zout.closeArchiveEntry();
+            }
+        }
+        return bos.toByteArray();
+    }
+
+    /** Copies resource, dropping toDrop entries and appending relAppends to 
the named .rels. */
+    private byte[] mutate(String resource, Map<String, String> relAppends, 
Set<String> toDrop)
+            throws Exception {
+        Map<String, String> remaining = new HashMap<>(relAppends);
+        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) {
+                String name = entry.getName();
+                if (toDrop.contains(name)) {
+                    continue;
+                }
+                zout.putArchiveEntry(new ZipArchiveEntry(name));
+                String relXml = remaining.remove(name);
+                if (relXml != null) {
+                    //append to the existing .rels, keeping the rest
+                    String rels = new String(IOUtils.toByteArray(zin), 
StandardCharsets.UTF_8);
+                    zout.write(rels.replace(RELS_CLOSE, relXml + RELS_CLOSE)
+                            .getBytes(StandardCharsets.UTF_8));
+                } else {
+                    IOUtils.copy(zin, zout);
+                }
+                zout.closeArchiveEntry();
+            }
+            //.rels parts the package didn't have at all
+            for (Map.Entry<String, String> e : remaining.entrySet()) {
+                zout.putArchiveEntry(new ZipArchiveEntry(e.getKey()));
+                String rels = "<?xml version=\"1.0\" encoding=\"UTF-8\" 
standalone=\"yes\"?>" +
+                        "<Relationships xmlns=\"" + RELS_NS + "\">" + 
e.getValue() + RELS_CLOSE;
+                zout.write(rels.getBytes(StandardCharsets.UTF_8));
+                zout.closeArchiveEntry();
+            }
+        }
+        return bos.toByteArray();
+    }
+}
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/SXWPFMissingRelatedPartTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ooxml/SXWPFMissingRelatedPartTest.java
new file mode 100644
index 0000000000..c50ad1b5bd
--- /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/SXWPFMissingRelatedPartTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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.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.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.parser.ParseContext;
+
+/**
+ * A docx that declares a relationship to an optional part (numbering.xml, 
settings.xml, ...)
+ * whose target is missing makes POI's getRelatedPart throw an unchecked
+ * IllegalArgumentException. The SAX docx extractor must skip it, not abort 
the whole parse.
+ * Regression guard for the 3.3.2 tika-eval finding: 740 docx crashed this way.
+ */
+public class SXWPFMissingRelatedPartTest extends TikaTest {
+
+    @Test
+    public void testMissingNumberingPart() throws Exception {
+        assertParsesWithoutPart("word/numbering.xml");
+    }
+
+    @Test
+    public void testMissingSettingsPart() throws Exception {
+        assertParsesWithoutPart("word/settings.xml");
+    }
+
+    @Test
+    public void testMissingStylesPart() throws Exception {
+        assertParsesWithoutPart("word/styles.xml");
+    }
+
+    @Test
+    public void testMissingWebSettingsPart() throws Exception {
+        assertParsesWithoutPart("word/webSettings.xml");
+    }
+
+    /** Drops partToDrop (leaving its dangling relationship) and asserts the 
docx still parses. */
+    private void assertParsesWithoutPart(String partToDrop) throws Exception {
+        byte[] docx = docxWithoutPart("testWORD_numbered_list.docx", 
partToDrop);
+        List<Metadata> metadataList;
+        try (TikaInputStream tis = TikaInputStream.get(docx)) {
+            metadataList = getRecursiveMetadata(tis, new Metadata(), new 
ParseContext(), false);
+        }
+        assertEquals(1, metadataList.size(), "dropped " + partToDrop);
+        Metadata m = metadataList.get(0);
+        
assertEquals("application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+                m.get(Metadata.CONTENT_TYPE), "dropped " + partToDrop);
+        String content = m.get(TikaCoreProperties.TIKA_CONTENT);
+        assertNotNull(content, "dropped " + partToDrop);
+        //body text still recovered, not lost to the abort
+        assertContains("This is another list", content);
+        assertContains("Within cell 1", content);
+    }
+
+    /** Copies the resource docx, omitting a single zip entry. */
+    private byte[] docxWithoutPart(String resource, String entryName) 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(entryName)) {
+                    continue;
+                }
+                zout.putArchiveEntry(new ZipArchiveEntry(entry.getName()));
+                IOUtils.copy(zin, zout);
+                zout.closeArchiveEntry();
+            }
+        }
+        return bos.toByteArray();
+    }
+}
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/SXWPFNonStandardNumberingTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ooxml/SXWPFNonStandardNumberingTest.java
new file mode 100644
index 0000000000..116dd544a7
--- /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/SXWPFNonStandardNumberingTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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 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.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.parser.ParseContext;
+
+/**
+ * XWPFNumberingShim sizes a dense per-level array to the largest w:ilvl it 
sees. An
+ * out-of-range ilvl (OOXML defines only levels 0-8) would ask for a 
~2-billion-element
+ * array and OOM -- an Error that escapes every catch in the chain and drops 
the file.
+ * The out-of-range level must be ignored.
+ */
+public class SXWPFNonStandardNumberingTest extends TikaTest {
+
+    // A well-formed w:lvl with an out-of-range ilvl, injected into an 
abstractNum.
+    private static final String OUT_OF_RANGE_LVL =
+            "<w:lvl w:ilvl=\"2000000000\">" +
+            "<w:start w:val=\"1\"/><w:numFmt w:val=\"decimal\"/><w:lvlText 
w:val=\"%1.\"/>" +
+            "</w:lvl>";
+
+    @Test
+    public void testOutOfRangeIlvlDoesNotOOM() throws Exception {
+        byte[] docx = docxWithInjectedLevel("testWORD_numbered_list.docx");
+        List<Metadata> metadataList;
+        try (TikaInputStream tis = TikaInputStream.get(docx)) {
+            metadataList = getRecursiveMetadata(tis, new Metadata(), new 
ParseContext(), false);
+        }
+        assertEquals(1, metadataList.size());
+        Metadata m = metadataList.get(0);
+        
assertEquals("application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+                m.get(Metadata.CONTENT_TYPE));
+        //the out-of-range level is ignored; the real list content still comes 
through
+        assertContains("This is another list", 
m.get(TikaCoreProperties.TIKA_CONTENT));
+    }
+
+    // Injects OUT_OF_RANGE_LVL just before the first </w:abstractNum> in 
numbering.xml.
+    private byte[] docxWithInjectedLevel(String resource) 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) {
+                zout.putArchiveEntry(new ZipArchiveEntry(entry.getName()));
+                if ("word/numbering.xml".equals(entry.getName())) {
+                    String xml = new String(IOUtils.toByteArray(zin), 
StandardCharsets.UTF_8);
+                    xml = xml.replaceFirst("</w:abstractNum>", 
OUT_OF_RANGE_LVL + "</w:abstractNum>");
+                    zout.write(xml.getBytes(StandardCharsets.UTF_8));
+                } else {
+                    IOUtils.copy(zin, zout);
+                }
+                zout.closeArchiveEntry();
+            }
+        }
+        return bos.toByteArray();
+    }
+}
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/XSSFBMalformedTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ooxml/XSSFBMalformedTest.java
new file mode 100644
index 0000000000..f419d72db5
--- /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/XSSFBMalformedTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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 java.io.ByteArrayOutputStream;
+import java.util.List;
+import java.util.Set;
+
+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.Metadata;
+import org.apache.tika.parser.ParseContext;
+
+/**
+ * The binary xlsb path must degrade the same way the xlsx path does when a 
part is missing.
+ * These drop parts a malformed/truncated xlsb might lack and assert the 
workbook still parses.
+ */
+public class XSSFBMalformedTest extends TikaTest {
+
+    private static final String XLSB_CT =
+            "application/vnd.ms-excel.sheet.binary.macroenabled.12";
+
+    // Cells still carry SST indices, but the string table is gone: 
getItemAt(idx) is out
+    // of range for the empty table -- must not IndexOutOfBounds out of the 
whole workbook.
+    @Test
+    public void testMissingSharedStrings() throws Exception {
+        Metadata m = parseWithout("testEXCEL.xlsb", 
Set.of("xl/sharedStrings.bin"));
+        assertEquals(XLSB_CT, m.get(Metadata.CONTENT_TYPE));
+    }
+
+    // A workbook that references a sheet part missing from the package.
+    @Test
+    public void testMissingSheetPart() throws Exception {
+        Metadata m = parseWithout("testEXCEL.xlsb", 
Set.of("xl/worksheets/sheet1.bin"));
+        assertEquals(XLSB_CT, m.get(Metadata.CONTENT_TYPE));
+    }
+
+    private Metadata parseWithout(String resource, Set<String> toDrop) throws 
Exception {
+        byte[] xlsb = drop(resource, toDrop);
+        List<Metadata> metadataList;
+        try (TikaInputStream tis = TikaInputStream.get(xlsb)) {
+            metadataList = getRecursiveMetadata(tis, new Metadata(), new 
ParseContext(), false);
+        }
+        assertEquals(1, metadataList.size());
+        return metadataList.get(0);
+    }
+
+    private byte[] drop(String resource, Set<String> toDrop) 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 (toDrop.contains(entry.getName())) {
+                    continue;
+                }
+                zout.putArchiveEntry(new ZipArchiveEntry(entry.getName()));
+                IOUtils.copy(zin, zout);
+                zout.closeArchiveEntry();
+            }
+        }
+        return bos.toByteArray();
+    }
+}
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/XSSFMalformedSheetTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ooxml/XSSFMalformedSheetTest.java
new file mode 100644
index 0000000000..cdc5dfe760
--- /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/XSSFMalformedSheetTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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 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.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.parser.ParseContext;
+
+/**
+ * The SAX xlsx sheet handler must not abort the whole workbook on a malformed 
cell/row.
+ * Each case injects one non-standard construct into sheet1.xml and asserts 
the workbook
+ * still parses (other cells' numbers still come through).
+ */
+public class XSSFMalformedSheetTest extends TikaTest {
+
+    // A stable numeric cell present in testEXCEL.xlsx, recovered when parsing 
survives.
+    private static final String SURVIVES = "<td>144</td>";
+
+    @Test
+    public void testNonNumericRowNumber() throws Exception {
+        Metadata m = parseWithSheetEdit("<row r=\"1\"", "<row r=\"x\"");
+        assertContains(SURVIVES, m.get(TikaCoreProperties.TIKA_CONTENT));
+    }
+
+    @Test
+    public void testNonNumericStyleIndex() throws Exception {
+        //B6 is a numeric cell (no t) -- the style-index parse only runs on 
that branch
+        Metadata m = parseWithSheetEdit("<c r=\"B6\">", "<c r=\"B6\" 
s=\"x\">");
+        assertContains(SURVIVES, m.get(TikaCoreProperties.TIKA_CONTENT));
+    }
+
+    @Test
+    public void testBooleanCellEmptyValue() throws Exception {
+        Metadata m = parseWithSheetEdit("<c r=\"B6\"><v>1</v></c>", "<c 
r=\"B6\" t=\"b\"><v></v></c>");
+        assertContains(SURVIVES, m.get(TikaCoreProperties.TIKA_CONTENT));
+    }
+
+    @Test
+    public void testMalformedCellReference() throws Exception {
+        Metadata m = parseWithSheetEdit("<c r=\"B6\">", "<c r=\"1A\">");
+        assertContains(SURVIVES, m.get(TikaCoreProperties.TIKA_CONTENT));
+    }
+
+    private Metadata parseWithSheetEdit(String find, String replace) throws 
Exception {
+        byte[] xlsx = editSheet1("testEXCEL.xlsx", find, replace);
+        List<Metadata> metadataList;
+        try (TikaInputStream tis = TikaInputStream.get(xlsx)) {
+            metadataList = getRecursiveMetadata(tis, new Metadata(), new 
ParseContext(), false);
+        }
+        assertEquals(1, metadataList.size());
+        return metadataList.get(0);
+    }
+
+    private byte[] editSheet1(String resource, String find, String replace) 
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) {
+                zout.putArchiveEntry(new ZipArchiveEntry(entry.getName()));
+                if ("xl/worksheets/sheet1.xml".equals(entry.getName())) {
+                    String xml = new String(IOUtils.toByteArray(zin), 
StandardCharsets.UTF_8);
+                    zout.write(xml.replace(find, 
replace).getBytes(StandardCharsets.UTF_8));
+                } else {
+                    IOUtils.copy(zin, zout);
+                }
+                zout.closeArchiveEntry();
+            }
+        }
+        return bos.toByteArray();
+    }
+}

Reply via email to