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