This is an automated email from the ASF dual-hosted git repository. tballison pushed a commit to branch sxwpf-numbering-settings-guard-3x in repository https://gitbox.apache.org/repos/asf/tika.git
commit fb344abb52f9cbad24c5cc4ae7cdc69d9dabc935 Author: tallison <[email protected]> AuthorDate: Fri Jul 10 16:08:52 2026 -0400 TIKA-4692 -- bug fixes --- .../ooxml/SXWPFWordExtractorDecorator.java | 24 ++-- .../ooxml/SXWPFMissingRelatedPartTest.java | 122 +++++++++++++++++++++ 2 files changed, 138 insertions(+), 8 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/ooxml/SXWPFWordExtractorDecorator.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/SXWPFWordExtractorDecorator.java index 38426a1279..86cdb6e03e 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/SXWPFWordExtractorDecorator.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/SXWPFWordExtractorDecorator.java @@ -194,7 +194,7 @@ public class SXWPFWordExtractorDecorator extends AbstractOOXMLExtractor { PackageRelationshipCollection settingsRels = documentPart.getRelationshipsByType(SETTINGS_RELATION); if (settingsRels != null && settingsRels.size() > 0) { - PackagePart settingsPart = documentPart.getRelatedPart(settingsRels.getRelationship(0)); + PackagePart settingsPart = safeGetRelatedPart(documentPart, settingsRels.getRelationship(0)); if (settingsPart != null) { try (InputStream is = settingsPart.getInputStream()) { WordSettingsHandler handler = new WordSettingsHandler(xhtml); @@ -214,7 +214,7 @@ public class SXWPFWordExtractorDecorator extends AbstractOOXMLExtractor { PackageRelationshipCollection webSettingsRels = documentPart.getRelationshipsByType(WEB_SETTINGS_RELATION); if (webSettingsRels != null && webSettingsRels.size() > 0) { - PackagePart webSettingsPart = documentPart.getRelatedPart(webSettingsRels.getRelationship(0)); + PackagePart webSettingsPart = safeGetRelatedPart(documentPart, webSettingsRels.getRelationship(0)); if (webSettingsPart != null) { try (InputStream is = webSettingsPart.getInputStream()) { WebSettingsHandler handler = new WebSettingsHandler(xhtml); @@ -272,7 +272,10 @@ public class SXWPFWordExtractorDecorator extends AbstractOOXMLExtractor { if (headersPRC != null) { for (int i = 0; i < headersPRC.size(); i++) { PackagePart header = - documentPart.getRelatedPart(headersPRC.getRelationship(i)); + safeGetRelatedPart(documentPart, headersPRC.getRelationship(i)); + if (header == null) { + continue; + } handlePart(header, styles, listManager, xhtml, OOXMLInlineBodyPartMap.EMPTY); } @@ -305,7 +308,10 @@ public class SXWPFWordExtractorDecorator extends AbstractOOXMLExtractor { if (prc != null) { for (int i = 0; i < prc.size(); i++) { PackagePart packagePart = - documentPart.getRelatedPart(prc.getRelationship(i)); + safeGetRelatedPart(documentPart, prc.getRelationship(i)); + if (packagePart == null) { + continue; + } handlePart(packagePart, styles, listManager, xhtml, OOXMLInlineBodyPartMap.EMPTY); } @@ -457,7 +463,7 @@ public class SXWPFWordExtractorDecorator extends AbstractOOXMLExtractor { if (stylesRelationShip == null) { return null; } - PackagePart stylesPart = packagePart.getRelatedPart(stylesRelationShip); + PackagePart stylesPart = safeGetRelatedPart(packagePart, stylesRelationShip); if (stylesPart == null) { return null; } @@ -477,7 +483,7 @@ public class SXWPFWordExtractorDecorator extends AbstractOOXMLExtractor { if (numberingRelationShip == null) { return null; } - PackagePart numberingPart = packagePart.getRelatedPart(numberingRelationShip); + PackagePart numberingPart = safeGetRelatedPart(packagePart, numberingRelationShip); if (numberingPart == null) { return null; } @@ -518,8 +524,10 @@ public class SXWPFWordExtractorDecorator extends AbstractOOXMLExtractor { if (prc != null) { for (int i = 0; i < prc.size(); i++) { PackagePart packagePart = - documentPart.getRelatedPart(prc.getRelationship(i)); - relatedParts.add(packagePart); + safeGetRelatedPart(documentPart, prc.getRelationship(i)); + if (packagePart != null) { + relatedParts.add(packagePart); + } } } } catch (InvalidFormatException e) { 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..dd7f807757 --- /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,122 @@ +/* + * 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.io.InputStream; +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.metadata.Metadata; +import org.apache.tika.metadata.TikaCoreProperties; +import org.apache.tika.parser.ParseContext; +import org.apache.tika.parser.microsoft.OfficeParserConfig; + +/** + * A docx may declare a relationship to an optional part (numbering.xml, + * settings.xml, ...) whose target is missing from the package -- a truncated or + * otherwise malformed file. POI's {@code PackagePart.getRelatedPart} then throws + * an unchecked {@code IllegalArgumentException}. The streaming (SAX) docx + * extractor must skip the missing part and keep going rather than aborting the + * whole parse (which also drops the file's embedded documents). + * + * <p>Regression guard for the 3.3.2 tika-eval finding: 740 docx crashed on a + * missing numbering.xml (484) or settings.xml (254) because those + * getRelatedPart calls bypassed {@code safeGetRelatedPart}. + */ +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"); + } + + /** + * Strips {@code partToDrop} from a known-good docx (leaving the dangling + * relationship in document.xml.rels) and asserts the SAX docx extractor + * still parses it -- without the fix, suppressException=false rethrows the + * IllegalArgumentException and this fails. + */ + private void assertParsesWithoutPart(String partToDrop) throws Exception { + byte[] docx = docxWithoutPart("testWORD_numbered_list.docx", partToDrop); + List<Metadata> metadataList; + try (InputStream is = new ByteArrayInputStream(docx)) { + metadataList = getRecursiveMetadata(is, new Metadata(), saxDocxContext(), 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 is still recovered, not lost to a catastrophic abort + assertContains("This is another list", content); + assertContains("Within cell 1", content); + } + + private ParseContext saxDocxContext() { + ParseContext pc = new ParseContext(); + OfficeParserConfig config = new OfficeParserConfig(); + config.setUseSAXDocxExtractor(true); + pc.set(OfficeParserConfig.class, config); + return pc; + } + + /** 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(); + } +}
