blob: c904ea3f9b60afa692ab2206af8669752d330a7a [file] [log] [blame]
Tobias Thierer878c77b2019-08-18 15:19:45 +01001/*
2 * Copyright (C) 2019 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.content.type;
18
19import libcore.net.MimeMap;
20
21import java.io.BufferedReader;
22import java.io.IOException;
23import java.io.InputStreamReader;
24import java.util.ArrayList;
25import java.util.HashMap;
26import java.util.List;
27import java.util.Map;
28import java.util.regex.Pattern;
29
30/**
31 * Default implementation of {@link MimeMap}, a bidirectional mapping between
32 * MIME types and file extensions.
33 *
34 * This default mapping is loaded from data files that start with some mappings
35 * recognized by IANA plus some custom extensions and overrides.
36 *
37 * @hide
38 */
39public class MimeMapImpl extends MimeMap {
40
41 /**
42 * Creates and returns a new {@link MimeMapImpl} instance that implements.
43 * Android's default mapping between MIME types and extensions.
44 */
45 public static MimeMapImpl createDefaultInstance() {
46 return parseFromResources("/mime.types", "/android.mime.types");
47 }
48
49 private static final Pattern SPLIT_PATTERN = Pattern.compile("\\s+");
50
51 /**
52 * Note: These maps only contain lowercase keys/values, regarded as the
53 * {@link #toLowerCase(String) canonical form}.
54 *
55 * <p>This is the case for both extensions and MIME types. The mime.types
56 * data file contains examples of mixed-case MIME types, but some applications
57 * use the lowercase version of these same types. RFC 2045 section 2 states
58 * that MIME types are case insensitive.
59 */
60 private final Map<String, String> mMimeTypeToExtension;
61 private final Map<String, String> mExtensionToMimeType;
62
63 public MimeMapImpl(Map<String, String> mimeTypeToExtension,
64 Map<String, String> extensionToMimeType) {
65 this.mMimeTypeToExtension = new HashMap<>(mimeTypeToExtension);
66 for (Map.Entry<String, String> entry : mimeTypeToExtension.entrySet()) {
67 checkValidMimeType(entry.getKey());
68 checkValidExtension(entry.getValue());
69 }
70 this.mExtensionToMimeType = new HashMap<>(extensionToMimeType);
71 for (Map.Entry<String, String> entry : extensionToMimeType.entrySet()) {
72 checkValidExtension(entry.getKey());
73 checkValidMimeType(entry.getValue());
74 }
75 }
76
77 private static void checkValidMimeType(String s) {
78 if (MimeMap.isNullOrEmpty(s) || !s.equals(MimeMap.toLowerCase(s))) {
79 throw new IllegalArgumentException("Invalid MIME type: " + s);
80 }
81 }
82
83 private static void checkValidExtension(String s) {
84 if (MimeMap.isNullOrEmpty(s) || !s.equals(MimeMap.toLowerCase(s))) {
85 throw new IllegalArgumentException("Invalid extension: " + s);
86 }
87 }
88
89 static MimeMapImpl parseFromResources(String... resourceNames) {
90 Map<String, String> mimeTypeToExtension = new HashMap<>();
91 Map<String, String> extensionToMimeType = new HashMap<>();
92 for (String resourceName : resourceNames) {
93 parseTypes(mimeTypeToExtension, extensionToMimeType, resourceName);
94 }
95 return new MimeMapImpl(mimeTypeToExtension, extensionToMimeType);
96 }
97
98 /**
99 * An element of a *mime.types file: A MIME type or an extension, with an optional
100 * prefix of "?" (if not overriding an earlier value).
101 */
102 private static class Element {
103 public final boolean keepExisting;
104 public final String s;
105
106 Element(boolean keepExisting, String value) {
107 this.keepExisting = keepExisting;
108 this.s = toLowerCase(value);
109 if (value.isEmpty()) {
110 throw new IllegalArgumentException();
111 }
112 }
113
114 public String toString() {
115 return keepExisting ? ("?" + s) : s;
116 }
117 }
118
119 private static String maybePut(Map<String, String> map, Element keyElement, String value) {
120 if (keyElement.keepExisting) {
121 return map.putIfAbsent(keyElement.s, value);
122 } else {
123 return map.put(keyElement.s, value);
124 }
125 }
126
127 private static void parseTypes(Map<String, String> mimeTypeToExtension,
128 Map<String, String> extensionToMimeType, String resource) {
129 try (BufferedReader r = new BufferedReader(
130 new InputStreamReader(MimeMapImpl.class.getResourceAsStream(resource)))) {
131 String line;
132 while ((line = r.readLine()) != null) {
133 int commentPos = line.indexOf('#');
134 if (commentPos >= 0) {
135 line = line.substring(0, commentPos);
136 }
137 line = line.trim();
138 // The first time a MIME type is encountered it is mapped to the first extension
139 // listed in its line. The first time an extension is encountered it is mapped
140 // to the MIME type.
141 //
142 // When encountering a previously seen MIME type or extension, then by default
143 // the later ones override earlier mappings (put() semantics); however if a MIME
144 // type or extension is prefixed with '?' then any earlier mapping _from_ that
145 // MIME type / extension is kept (putIfAbsent() semantics).
146 final String[] split = SPLIT_PATTERN.split(line);
147 if (split.length <= 1) {
148 // Need mimeType + at least one extension to make a mapping.
149 // "mime.types" files may also contain lines with just a mimeType without
150 // an extension but we skip them as they provide no mapping info.
151 continue;
152 }
153 List<Element> lineElements = new ArrayList<>(split.length);
154 for (String s : split) {
155 boolean keepExisting = s.startsWith("?");
156 if (keepExisting) {
157 s = s.substring(1);
158 }
159 if (s.isEmpty()) {
160 throw new IllegalArgumentException("Invalid entry in '" + line + "'");
161 }
162 lineElements.add(new Element(keepExisting, s));
163 }
164
165 // MIME type -> first extension (one mapping)
166 // This will override any earlier mapping from this MIME type to another
167 // extension, unless this MIME type was prefixed with '?'.
168 Element mimeElement = lineElements.get(0);
169 List<Element> extensionElements = lineElements.subList(1, lineElements.size());
170 String firstExtension = extensionElements.get(0).s;
171 maybePut(mimeTypeToExtension, mimeElement, firstExtension);
172
173 // extension -> MIME type (one or more mappings).
174 // This will override any earlier mapping from this extension to another
175 // MIME type, unless this extension was prefixed with '?'.
176 for (Element extensionElement : extensionElements) {
177 maybePut(extensionToMimeType, extensionElement, mimeElement.s);
178 }
179 }
180 } catch (IOException | RuntimeException e) {
181 throw new RuntimeException("Failed to parse " + resource, e);
182 }
183 }
184
185 @Override
186 protected String guessExtensionFromLowerCaseMimeType(String mimeType) {
187 return mMimeTypeToExtension.get(mimeType);
188 }
189
190 @Override
191 protected String guessMimeTypeFromLowerCaseExtension(String extension) {
192 return mExtensionToMimeType.get(extension);
193 }
194}