|
| 1 | +/* |
| 2 | + * Licensed to the Apache Software Foundation (ASF) under one or more |
| 3 | + * contributor license agreements. See the NOTICE file distributed with |
| 4 | + * this work for additional information regarding copyright ownership. |
| 5 | + * The ASF licenses this file to You under the Apache License, Version 2.0 |
| 6 | + * (the "License"); you may not use this file except in compliance with |
| 7 | + * the License. You may obtain a copy of the License at |
| 8 | + * |
| 9 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | + * |
| 11 | + * Unless required by applicable law or agreed to in writing, software |
| 12 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | + * See the License for the specific language governing permissions and |
| 15 | + * limitations under the License. |
| 16 | + */ |
| 17 | +package org.apache.tika.parser.microsoft.ooxml; |
| 18 | + |
| 19 | +import static org.junit.jupiter.api.Assertions.assertEquals; |
| 20 | +import static org.junit.jupiter.api.Assertions.assertNotNull; |
| 21 | + |
| 22 | +import java.io.ByteArrayOutputStream; |
| 23 | +import java.nio.charset.StandardCharsets; |
| 24 | +import java.util.List; |
| 25 | + |
| 26 | +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; |
| 27 | +import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; |
| 28 | +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; |
| 29 | +import org.apache.commons.io.IOUtils; |
| 30 | +import org.junit.jupiter.api.Test; |
| 31 | + |
| 32 | +import org.apache.tika.TikaTest; |
| 33 | +import org.apache.tika.io.TikaInputStream; |
| 34 | +import org.apache.tika.metadata.HttpHeaders; |
| 35 | +import org.apache.tika.metadata.Metadata; |
| 36 | +import org.apache.tika.metadata.TikaCoreProperties; |
| 37 | + |
| 38 | +/** |
| 39 | + * An OOXML package may declare a relationship whose target part is missing -- a truncated |
| 40 | + * or otherwise malformed file. POI's {@code PackagePart.getRelatedPart} then throws an |
| 41 | + * unchecked {@code IllegalArgumentException}, which |
| 42 | + * {@link OOXMLExtractorFactory} converts into a TikaException: the whole file aborts and |
| 43 | + * even the text already extracted is lost. Every such call must go through |
| 44 | + * {@link AbstractOOXMLExtractor#safeGetRelatedPart}. |
| 45 | + * |
| 46 | + * <p>Same failure class as the 3.3.2 docx regression (740 files crashed on a missing |
| 47 | + * numbering.xml/settings.xml). 3.3.2 guards these four xlsx/vsdx sites; 4.0.0 shipped |
| 48 | + * without them (TIKA-4879). |
| 49 | + */ |
| 50 | +public class OOXMLMissingRelatedPartTest extends TikaTest { |
| 51 | + |
| 52 | + private static final String XLSX_TYPE = |
| 53 | + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; |
| 54 | + private static final String VSDX_TYPE = "application/vnd.ms-visio.drawing"; |
| 55 | + |
| 56 | + @Test |
| 57 | + public void testDanglingThreadedCommentRelationship() throws Exception { |
| 58 | + //XSSFExcelExtractorDecorator.getThreadedComments |
| 59 | + byte[] xlsx = withExtraRelationship("testComment.xlsx", |
| 60 | + "xl/worksheets/_rels/sheet1.xml.rels", |
| 61 | + "http://schemas.microsoft.com/office/2017/10/relationships/threadedComment", |
| 62 | + "../threadedComments/threadedComment1.xml"); |
| 63 | + assertSheetTextSurvives(xlsx); |
| 64 | + } |
| 65 | + |
| 66 | + @Test |
| 67 | + public void testDanglingPersonRelationship() throws Exception { |
| 68 | + //XSSFExcelExtractorDecorator.getPersons |
| 69 | + byte[] xlsx = withExtraRelationship("testComment.xlsx", "xl/_rels/workbook.xml.rels", |
| 70 | + "http://schemas.microsoft.com/office/2017/10/relationships/person", |
| 71 | + "persons/person.xml"); |
| 72 | + assertSheetTextSurvives(xlsx); |
| 73 | + } |
| 74 | + |
| 75 | + @Test |
| 76 | + public void testMissingVisioPage() throws Exception { |
| 77 | + //VSDXExtractorDecorator.getPageParts, the per-page loop |
| 78 | + assertVisioParseCompletes(withoutEntry("testVISIO.vsdx", "visio/pages/page1.xml")); |
| 79 | + } |
| 80 | + |
| 81 | + @Test |
| 82 | + public void testMissingVisioPagesPart() throws Exception { |
| 83 | + //VSDXExtractorDecorator.getRelatedPart(PackagePart, String), document.xml -> pages.xml |
| 84 | + assertVisioParseCompletes(withoutEntry("testVISIO.vsdx", "visio/pages/pages.xml")); |
| 85 | + } |
| 86 | + |
| 87 | + /** |
| 88 | + * The pages are unreachable, but the parse must still run to completion: the EMF |
| 89 | + * thumbnail is emitted after buildXHTML, so its presence proves we did not abort. |
| 90 | + */ |
| 91 | + private void assertVisioParseCompletes(byte[] vsdx) throws Exception { |
| 92 | + List<Metadata> metadataList = assertParses(vsdx, VSDX_TYPE); |
| 93 | + assertEquals(2, metadataList.size()); |
| 94 | + assertEquals("image/emf", metadataList.get(1).get(HttpHeaders.CONTENT_TYPE)); |
| 95 | + } |
| 96 | + |
| 97 | + /** The dangling relationship must not cost us the sheet text that parsed fine. */ |
| 98 | + private void assertSheetTextSurvives(byte[] xlsx) throws Exception { |
| 99 | + List<Metadata> metadataList = assertParses(xlsx, XLSX_TYPE); |
| 100 | + assertEquals(1, metadataList.size()); |
| 101 | + assertContains("Here is some text", |
| 102 | + metadataList.get(0).get(TikaCoreProperties.TIKA_CONTENT)); |
| 103 | + } |
| 104 | + |
| 105 | + private List<Metadata> assertParses(byte[] bytes, String expectedType) throws Exception { |
| 106 | + List<Metadata> metadataList; |
| 107 | + try (TikaInputStream tis = TikaInputStream.get(bytes)) { |
| 108 | + //suppressException=false: an escaping IllegalArgumentException fails the test here |
| 109 | + metadataList = getRecursiveMetadata(tis, false); |
| 110 | + } |
| 111 | + Metadata m = metadataList.get(0); |
| 112 | + assertEquals(expectedType, m.get(HttpHeaders.CONTENT_TYPE)); |
| 113 | + assertNotNull(m.get(TikaCoreProperties.TIKA_CONTENT)); |
| 114 | + return metadataList; |
| 115 | + } |
| 116 | + |
| 117 | + /** Copies the resource, omitting one zip entry and leaving its relationship dangling. */ |
| 118 | + private byte[] withoutEntry(String resource, String entryName) throws Exception { |
| 119 | + return copy(resource, entryName, null, null, null); |
| 120 | + } |
| 121 | + |
| 122 | + /** Copies the resource, appending a relationship whose target is not in the package. */ |
| 123 | + private byte[] withExtraRelationship(String resource, String relsEntry, String type, |
| 124 | + String target) throws Exception { |
| 125 | + return copy(resource, null, relsEntry, type, target); |
| 126 | + } |
| 127 | + |
| 128 | + private byte[] copy(String resource, String dropEntry, String relsEntry, String type, |
| 129 | + String target) throws Exception { |
| 130 | + ByteArrayOutputStream bos = new ByteArrayOutputStream(); |
| 131 | + try (ZipArchiveInputStream zin = |
| 132 | + new ZipArchiveInputStream(getResourceAsStream("/test-documents/" + resource)); |
| 133 | + ZipArchiveOutputStream zout = new ZipArchiveOutputStream(bos)) { |
| 134 | + ZipArchiveEntry entry; |
| 135 | + while ((entry = zin.getNextEntry()) != null) { |
| 136 | + if (entry.getName().equals(dropEntry)) { |
| 137 | + continue; |
| 138 | + } |
| 139 | + byte[] data = IOUtils.toByteArray(zin); |
| 140 | + if (entry.getName().equals(relsEntry)) { |
| 141 | + String rels = new String(data, StandardCharsets.UTF_8); |
| 142 | + String injected = "<Relationship Id=\"rIdMissingTarget\" Type=\"" + type + |
| 143 | + "\" Target=\"" + target + "\"/></Relationships>"; |
| 144 | + data = rels.replace("</Relationships>", injected) |
| 145 | + .getBytes(StandardCharsets.UTF_8); |
| 146 | + } |
| 147 | + zout.putArchiveEntry(new ZipArchiveEntry(entry.getName())); |
| 148 | + zout.write(data); |
| 149 | + zout.closeArchiveEntry(); |
| 150 | + } |
| 151 | + } |
| 152 | + return bos.toByteArray(); |
| 153 | + } |
| 154 | +} |
0 commit comments