Skip to content

Commit df4b9af

Browse files
committed
TIKA-4808 -- fix extension calculations for embedded files
1 parent d4091f5 commit df4b9af

2 files changed

Lines changed: 151 additions & 27 deletions

File tree

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

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -149,40 +149,24 @@ public String getExtension(TikaInputStream is, Metadata metadata) {
149149
MimeTypes localMimeTypes = getMimeTypes();
150150

151151
MimeType mimeType = null;
152-
boolean detected = false;
153152
if (mimeString != null) {
154-
try {
155-
mimeType = localMimeTypes.forName(mimeString);
156-
} catch (MimeTypeException e) {
157-
//swallow
158-
}
153+
mimeType = getRegisteredMimeType(localMimeTypes, mimeString);
159154
}
160155
if (mimeType == null) {
161156
try {
162157
MediaType mediaType = getDetector().detect(is, metadata, context);
163-
mimeType = localMimeTypes.forName(mediaType.toString());
164-
detected = true;
165158
is.reset();
166-
} catch (IOException | MimeTypeException e) {
159+
//set or correct the mime type. Record what was detected, not the
160+
//registry match, which may have fallen back to the base type.
161+
metadata.set(Metadata.CONTENT_TYPE, mediaType.toString());
162+
mimeType = getRegisteredMimeType(localMimeTypes, mediaType.toString());
163+
} catch (IOException e) {
167164
//swallow
168165
}
169166
}
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();
176-
}
177-
return ".bin";
167+
return mimeType == null ? ".bin" : mimeType.getExtension();
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-
*/
186170
/**
187171
* Normalizes internal OCR routing media types (e.g., {@code image/ocr-png})
188172
* back to standard media types (e.g., {@code image/png}).
@@ -198,16 +182,36 @@ public static String normalizeMediaType(String mediaType) {
198182
return mediaType;
199183
}
200184

185+
/**
186+
* Looks up the file extension for a given media type string.
187+
*
188+
* @param mediaType the media type string (e.g., "image/png"), parameters allowed
189+
* @return the extension including the dot (e.g., ".png"), or empty string if unknown
190+
*/
201191
public static String getExtensionForMediaType(String mediaType) {
202192
if (mediaType == null) {
203193
return "";
204194
}
205-
mediaType = normalizeMediaType(mediaType);
195+
MimeType mimeType =
196+
getRegisteredMimeType(MimeTypes.getDefaultMimeTypes(),
197+
normalizeMediaType(mediaType));
198+
return mimeType == null ? "" : mimeType.getExtension();
199+
}
200+
201+
/**
202+
* Not {@link MimeTypes#forName(String)}: that registers a new, glob-less type for
203+
* any name it doesn't recognize, so <code>text/plain; charset=UTF-8</code> would
204+
* lose its extension and add a registry entry per charset seen. This prefers an
205+
* exact parameterized match (<code>application/dita+xml;format=map</code> is real)
206+
* and otherwise falls back to the base type.
207+
*
208+
* @return the registered type, or null if unknown or invalid
209+
*/
210+
private static MimeType getRegisteredMimeType(MimeTypes mimeTypes, String name) {
206211
try {
207-
MimeType mimeType = MimeTypes.getDefaultMimeTypes().forName(mediaType);
208-
return mimeType.getExtension();
212+
return mimeTypes.getRegisteredMimeType(name);
209213
} catch (MimeTypeException e) {
210-
return "";
214+
return null;
211215
}
212216
}
213217

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

0 commit comments

Comments
 (0)