Skip to content

Commit 48d56d6

Browse files
authored
TIKA-4808 -- fix extension calculations for embedded files
2 parents 738df4e + 99ce12a commit 48d56d6

3 files changed

Lines changed: 193 additions & 35 deletions

File tree

tika-core/src/main/java/org/apache/tika/extractor/EmbeddedDocumentUtil.java

Lines changed: 42 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -148,41 +148,29 @@ public String getExtension(TikaInputStream is, Metadata metadata) {
148148
//use the buffered mimetypes as default
149149
MimeTypes localMimeTypes = getMimeTypes();
150150

151-
MimeType mimeType = null;
152-
boolean detected = false;
153-
if (mimeString != null) {
154-
try {
155-
mimeType = localMimeTypes.forName(mimeString);
156-
} catch (MimeTypeException e) {
157-
//swallow
158-
}
159-
}
160-
if (mimeType == null) {
161-
try {
162-
MediaType mediaType = getDetector().detect(is, metadata, context);
163-
mimeType = localMimeTypes.forName(mediaType.toString());
164-
detected = true;
165-
is.reset();
166-
} catch (IOException | MimeTypeException e) {
167-
//swallow
168-
}
151+
//a parseable declared type wins, even if we have no glob for it. Don't
152+
//detect just because the registry lookup came back empty -- that would
153+
//overwrite a type the calling parser set deliberately.
154+
if (mimeString != null && MediaType.parse(mimeString) != null) {
155+
return extensionOf(getRegisteredMimeType(localMimeTypes, mimeString));
169156
}
170-
if (mimeType != null) {
171-
if (detected) {
172-
//set or correct the mime type
173-
metadata.set(Metadata.CONTENT_TYPE, mimeType.toString());
174-
}
175-
return mimeType.getExtension();
157+
try {
158+
MediaType mediaType = getDetector().detect(is, metadata, context);
159+
is.reset();
160+
//set or correct the mime type. Record what was detected, not the
161+
//registry match, which may have fallen back to the base type.
162+
metadata.set(Metadata.CONTENT_TYPE, mediaType.toString());
163+
return extensionOf(getRegisteredMimeType(localMimeTypes, mediaType.toString()));
164+
} catch (IOException e) {
165+
//swallow
176166
}
177167
return ".bin";
178168
}
179169

180-
/**
181-
* Looks up the file extension for a given media type string.
182-
*
183-
* @param mediaType the media type string (e.g., "image/png")
184-
* @return the extension including the dot (e.g., ".png"), or empty string if unknown
185-
*/
170+
private static String extensionOf(MimeType mimeType) {
171+
return mimeType == null ? "" : mimeType.getExtension();
172+
}
173+
186174
/**
187175
* Normalizes internal OCR routing media types (e.g., {@code image/ocr-png})
188176
* back to standard media types (e.g., {@code image/png}).
@@ -198,16 +186,36 @@ public static String normalizeMediaType(String mediaType) {
198186
return mediaType;
199187
}
200188

189+
/**
190+
* Looks up the file extension for a given media type string.
191+
*
192+
* @param mediaType the media type string (e.g., "image/png"), parameters allowed
193+
* @return the extension including the dot (e.g., ".png"), or empty string if unknown
194+
*/
201195
public static String getExtensionForMediaType(String mediaType) {
202196
if (mediaType == null) {
203197
return "";
204198
}
205-
mediaType = normalizeMediaType(mediaType);
199+
MimeType mimeType =
200+
getRegisteredMimeType(MimeTypes.getDefaultMimeTypes(),
201+
normalizeMediaType(mediaType));
202+
return mimeType == null ? "" : mimeType.getExtension();
203+
}
204+
205+
/**
206+
* Not {@link MimeTypes#forName(String)}: that registers a new, glob-less type for
207+
* any name it doesn't recognize, so <code>text/plain; charset=UTF-8</code> would
208+
* lose its extension and add a registry entry per charset seen. This prefers an
209+
* exact parameterized match (<code>application/dita+xml;format=map</code> is real)
210+
* and otherwise falls back to the base type.
211+
*
212+
* @return the registered type, or null if unknown or invalid
213+
*/
214+
private static MimeType getRegisteredMimeType(MimeTypes mimeTypes, String name) {
206215
try {
207-
MimeType mimeType = MimeTypes.getDefaultMimeTypes().forName(mediaType);
208-
return mimeType.getExtension();
216+
return mimeTypes.getRegisteredMimeType(name);
209217
} catch (MimeTypeException e) {
210-
return "";
218+
return null;
211219
}
212220
}
213221

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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.extractor;
18+
19+
import static java.nio.charset.StandardCharsets.UTF_8;
20+
import static org.junit.jupiter.api.Assertions.assertEquals;
21+
22+
import org.junit.jupiter.api.Test;
23+
24+
import org.apache.tika.io.TikaInputStream;
25+
import org.apache.tika.metadata.Metadata;
26+
import org.apache.tika.mime.MediaType;
27+
import org.apache.tika.mime.MimeTypes;
28+
import org.apache.tika.parser.ParseContext;
29+
30+
/**
31+
* TIKA-4808 -- a media type carrying parameters must still resolve to the
32+
* extension of its base type.
33+
*/
34+
public class EmbeddedDocumentUtilExtensionTest {
35+
36+
@Test
37+
public void testParametersDoNotSuppressExtension() {
38+
//the corpus regression: Pkcs7Parser refines the coarse family label to the
39+
//exact smime-type, which turned /embedded-2.p7s into /embedded-2
40+
assertEquals(".p7m",
41+
EmbeddedDocumentUtil.getExtensionForMediaType(
42+
"application/pkcs7-mime; smime-type=signed-data"));
43+
assertEquals(".txt",
44+
EmbeddedDocumentUtil.getExtensionForMediaType("text/plain; charset=UTF-8"));
45+
assertEquals(".js",
46+
EmbeddedDocumentUtil.getExtensionForMediaType(
47+
"text/javascript; charset=UTF-8"));
48+
assertEquals(".css",
49+
EmbeddedDocumentUtil.getExtensionForMediaType(
50+
"text/css; charset=ISO-2022-JP"));
51+
assertEquals(".html",
52+
EmbeddedDocumentUtil.getExtensionForMediaType(
53+
"text/html; charset=windows-1252"));
54+
}
55+
56+
@Test
57+
public void testUnparameterizedStillWorks() {
58+
assertEquals(".p7s",
59+
EmbeddedDocumentUtil.getExtensionForMediaType("application/pkcs7-signature"));
60+
assertEquals(".png", EmbeddedDocumentUtil.getExtensionForMediaType("image/png"));
61+
assertEquals(".txt", EmbeddedDocumentUtil.getExtensionForMediaType("text/plain"));
62+
}
63+
64+
/**
65+
* A registered type that genuinely has parameters must win over its base type.
66+
*/
67+
@Test
68+
public void testRegisteredParameterizedTypeWinsOverBaseType() {
69+
assertEquals(".ditamap",
70+
EmbeddedDocumentUtil.getExtensionForMediaType(
71+
"application/dita+xml;format=map"));
72+
assertEquals(".dita",
73+
EmbeddedDocumentUtil.getExtensionForMediaType(
74+
"application/dita+xml;format=topic"));
75+
}
76+
77+
@Test
78+
public void testUnknownAndNull() {
79+
assertEquals("", EmbeddedDocumentUtil.getExtensionForMediaType(null));
80+
assertEquals("",
81+
EmbeddedDocumentUtil.getExtensionForMediaType("application/tika-bogus-xyz"));
82+
}
83+
84+
@Test
85+
public void testOcrRoutingTypeIsNormalized() {
86+
assertEquals(".png", EmbeddedDocumentUtil.getExtensionForMediaType("image/ocr-png"));
87+
}
88+
89+
/**
90+
* The lookup must not register anything: forName() would add one glob-less entry
91+
* per distinct parameter value seen, which on a large crawl grows without bound.
92+
*/
93+
@Test
94+
public void testLookupDoesNotPolluteRegistry() {
95+
MimeTypes mimeTypes = MimeTypes.getDefaultMimeTypes();
96+
int before = mimeTypes.getMediaTypeRegistry().getTypes().size();
97+
for (int i = 0; i < 50; i++) {
98+
EmbeddedDocumentUtil.getExtensionForMediaType("text/plain; charset=made-up-" + i);
99+
}
100+
assertEquals(before, mimeTypes.getMediaTypeRegistry().getTypes().size());
101+
assertEquals(".txt",
102+
EmbeddedDocumentUtil.getExtensionForMediaType("text/plain; charset=made-up-0"));
103+
}
104+
105+
@Test
106+
public void testGeneratedResourceNameKeepsExtension() {
107+
assertEquals("embedded-2.p7m", EmbeddedDocumentUtil.generateResourceName(
108+
EmbeddedDocumentUtil.EmbeddedResourcePrefix.EMBEDDED, 2,
109+
"application/pkcs7-mime; smime-type=signed-data"));
110+
assertEquals("image-0.png", EmbeddedDocumentUtil.generateResourceName(
111+
EmbeddedDocumentUtil.EmbeddedResourcePrefix.IMAGE, 0, "image/png"));
112+
}
113+
114+
/**
115+
* A declared type that simply has no glob must not trigger detection -- that would
116+
* overwrite a CONTENT_TYPE the calling parser set deliberately.
117+
*/
118+
@Test
119+
public void testDeclaredUnregisteredTypeIsNotOverwritten() throws Exception {
120+
Metadata metadata = new Metadata();
121+
metadata.set(Metadata.CONTENT_TYPE, "application/tika-bogus-xyz");
122+
EmbeddedDocumentUtil util = new EmbeddedDocumentUtil(new ParseContext());
123+
try (TikaInputStream tis = TikaInputStream.get("%PDF-1.4\n".getBytes(UTF_8))) {
124+
assertEquals("", util.getExtension(tis, metadata));
125+
assertEquals("application/tika-bogus-xyz", metadata.get(Metadata.CONTENT_TYPE));
126+
}
127+
}
128+
129+
@Test
130+
public void testDeclaredParameterizedTypeResolvesAndIsPreserved() throws Exception {
131+
Metadata metadata = new Metadata();
132+
metadata.set(Metadata.CONTENT_TYPE, "text/plain; charset=UTF-8");
133+
EmbeddedDocumentUtil util = new EmbeddedDocumentUtil(new ParseContext());
134+
try (TikaInputStream tis = TikaInputStream.get("hello".getBytes(UTF_8))) {
135+
assertEquals(".txt", util.getExtension(tis, metadata));
136+
assertEquals("text/plain; charset=UTF-8", metadata.get(Metadata.CONTENT_TYPE));
137+
}
138+
}
139+
140+
/**
141+
* Guard the assumption the fix rests on: normalize() deliberately preserves
142+
* parameters, which is why forName() misses the registry for parameterized names.
143+
*/
144+
@Test
145+
public void testNormalizePreservesParameters() {
146+
MediaType withParams = MediaType.parse("text/plain; charset=UTF-8");
147+
assertEquals(withParams,
148+
MimeTypes.getDefaultMimeTypes().getMediaTypeRegistry().normalize(withParams));
149+
}
150+
}

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-mail-module/src/test/java/org/apache/tika/parser/mail/RFC822ParserTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ public void testExtractAttachments() throws Exception {
404404
assertEquals(null, metadataList.get(1).get(Metadata.CONTENT_DISPOSITION));
405405
assertEquals("attachment; filename=\"testPNG.png\"",
406406
metadataList.get(2).get(Metadata.CONTENT_DISPOSITION));
407-
assertEquals("/Test Attachment Email.eml/embedded-1",
407+
assertEquals("/Test Attachment Email.eml/embedded-1.txt",
408408
metadataList.get(1).get(TikaCoreProperties.FINAL_EMBEDDED_RESOURCE_PATH));
409409
assertEquals("/Test Attachment Email.eml/testPNG.png",
410410
metadataList.get(2).get(TikaCoreProperties.FINAL_EMBEDDED_RESOURCE_PATH));

0 commit comments

Comments
 (0)