OOXMLMissingRelatedPartTest.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.tika.parser.microsoft.ooxml;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Test;
import org.apache.tika.TikaTest;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.HttpHeaders;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TikaCoreProperties;
/**
* An OOXML package may declare a relationship whose target part is missing -- a truncated
* or otherwise malformed file. POI's {@code PackagePart.getRelatedPart} then throws an
* unchecked {@code IllegalArgumentException}, which
* {@link OOXMLExtractorFactory} converts into a TikaException: the whole file aborts and
* even the text already extracted is lost. Every such call must go through
* {@link AbstractOOXMLExtractor#safeGetRelatedPart}.
*
* <p>Same failure class as the 3.3.2 docx regression (740 files crashed on a missing
* numbering.xml/settings.xml). 3.3.2 guards these four xlsx/vsdx sites; 4.0.0 shipped
* without them (TIKA-4879).
*/
public class OOXMLMissingRelatedPartTest extends TikaTest {
private static final String XLSX_TYPE =
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
private static final String VSDX_TYPE = "application/vnd.ms-visio.drawing";
@Test
public void testDanglingThreadedCommentRelationship() throws Exception {
//XSSFExcelExtractorDecorator.getThreadedComments
byte[] xlsx = withExtraRelationship("testComment.xlsx",
"xl/worksheets/_rels/sheet1.xml.rels",
"http://schemas.microsoft.com/office/2017/10/relationships/threadedComment",
"../threadedComments/threadedComment1.xml");
assertSheetTextSurvives(xlsx);
}
@Test
public void testDanglingPersonRelationship() throws Exception {
//XSSFExcelExtractorDecorator.getPersons
byte[] xlsx = withExtraRelationship("testComment.xlsx", "xl/_rels/workbook.xml.rels",
"http://schemas.microsoft.com/office/2017/10/relationships/person",
"persons/person.xml");
assertSheetTextSurvives(xlsx);
}
@Test
public void testMissingVisioPage() throws Exception {
//VSDXExtractorDecorator.getPageParts, the per-page loop
assertVisioParseCompletes(withoutEntry("testVISIO.vsdx", "visio/pages/page1.xml"));
}
@Test
public void testMissingVisioPagesPart() throws Exception {
//VSDXExtractorDecorator.getRelatedPart(PackagePart, String), document.xml -> pages.xml
assertVisioParseCompletes(withoutEntry("testVISIO.vsdx", "visio/pages/pages.xml"));
}
/**
* The pages are unreachable, but the parse must still run to completion: the EMF
* thumbnail is emitted after buildXHTML, so its presence proves we did not abort.
*/
private void assertVisioParseCompletes(byte[] vsdx) throws Exception {
List<Metadata> metadataList = assertParses(vsdx, VSDX_TYPE);
assertEquals(2, metadataList.size());
assertEquals("image/emf", metadataList.get(1).get(HttpHeaders.CONTENT_TYPE));
}
/** The dangling relationship must not cost us the sheet text that parsed fine. */
private void assertSheetTextSurvives(byte[] xlsx) throws Exception {
List<Metadata> metadataList = assertParses(xlsx, XLSX_TYPE);
assertEquals(1, metadataList.size());
assertContains("Here is some text",
metadataList.get(0).get(TikaCoreProperties.TIKA_CONTENT));
}
private List<Metadata> assertParses(byte[] bytes, String expectedType) throws Exception {
List<Metadata> metadataList;
try (TikaInputStream tis = TikaInputStream.get(bytes)) {
//suppressException=false: an escaping IllegalArgumentException fails the test here
metadataList = getRecursiveMetadata(tis, false);
}
Metadata m = metadataList.get(0);
assertEquals(expectedType, m.get(HttpHeaders.CONTENT_TYPE));
assertNotNull(m.get(TikaCoreProperties.TIKA_CONTENT));
return metadataList;
}
/** Copies the resource, omitting one zip entry and leaving its relationship dangling. */
private byte[] withoutEntry(String resource, String entryName) throws Exception {
return copy(resource, entryName, null, null, null);
}
/** Copies the resource, appending a relationship whose target is not in the package. */
private byte[] withExtraRelationship(String resource, String relsEntry, String type,
String target) throws Exception {
return copy(resource, null, relsEntry, type, target);
}
private byte[] copy(String resource, String dropEntry, String relsEntry, String type,
String target) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (ZipArchiveInputStream zin =
new ZipArchiveInputStream(getResourceAsStream("/test-documents/" + resource));
ZipArchiveOutputStream zout = new ZipArchiveOutputStream(bos)) {
ZipArchiveEntry entry;
while ((entry = zin.getNextEntry()) != null) {
if (entry.getName().equals(dropEntry)) {
continue;
}
byte[] data = IOUtils.toByteArray(zin);
if (entry.getName().equals(relsEntry)) {
String rels = new String(data, StandardCharsets.UTF_8);
String injected = "<Relationship Id=\"rIdMissingTarget\" Type=\"" + type +
"\" Target=\"" + target + "\"/></Relationships>";
data = rels.replace("</Relationships>", injected)
.getBytes(StandardCharsets.UTF_8);
}
zout.putArchiveEntry(new ZipArchiveEntry(entry.getName()));
zout.write(data);
zout.closeArchiveEntry();
}
}
return bos.toByteArray();
}
}