From 53f15f39f82ef4c4bd99c6d22f3563bae0c35269 Mon Sep 17 00:00:00 2001 From: Tobias Thierer Date: Sun, 18 Aug 2019 15:19:45 +0100 Subject: Move default MimeMap implementation to frameworks. This CL topic moves the default MimeMap implementation to frameworks. Libcore starts with a minimal implementation sufficient to pass CtsLibcoreTestCases, but frameworks can inject the real implementation. Before this CL topic, the data files and logic (MimeMapImpl) were part of core-*.jar on device; after this CL, they instead live in framework.jar. Tests from MimeMapTest that check behavior of that default implementation also move to a non-libcore CTS test. Specifically, the logic and android.mime.types now live in frameworks/base/mime. The default implementation is injected into libcore from RuntimeInit. I chose to use a separate directory (frameworks/base/mime/) and build java_library target ("mimemap") in order to keep this as separate as possible from the rest of frameworks code, to make it as easy as possible to factor this out into a separate APEX module if we ever choose to do so. Planned work for follow-up CL: 1. Make CTS more opinionated, with a plan to assert that all of the default mappings are present. How exactly the expectated mapping will be bundled in CTS is still TBD. 2. Add a vendor.mime.types file (defaults to empty) where vendors can add additional mappings; I plan to make it such that mappings in that file are parsed last but never override any earlier mappings, as if each mime type / file extension was prefixed with '?'. 3. Perhaps enforce that public APIs android.webkit.MimeTypeMap and java.net.URLConnection.getFileNameMap() behave consistently with MimeMap.getDefault(). Test: atest CtsLibcoreTestCases Test: atest CtsMimeMapTestCases Bug: 136256059 Change-Id: Ib955699694d24a25c33ef2445443afb7c35ed9e7 --- mime/java/android/content/type/MimeMapImpl.java | 194 ++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 mime/java/android/content/type/MimeMapImpl.java (limited to 'mime/java') diff --git a/mime/java/android/content/type/MimeMapImpl.java b/mime/java/android/content/type/MimeMapImpl.java new file mode 100644 index 000000000000..c904ea3f9b60 --- /dev/null +++ b/mime/java/android/content/type/MimeMapImpl.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed 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 android.content.type; + +import libcore.net.MimeMap; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * Default implementation of {@link MimeMap}, a bidirectional mapping between + * MIME types and file extensions. + * + * This default mapping is loaded from data files that start with some mappings + * recognized by IANA plus some custom extensions and overrides. + * + * @hide + */ +public class MimeMapImpl extends MimeMap { + + /** + * Creates and returns a new {@link MimeMapImpl} instance that implements. + * Android's default mapping between MIME types and extensions. + */ + public static MimeMapImpl createDefaultInstance() { + return parseFromResources("/mime.types", "/android.mime.types"); + } + + private static final Pattern SPLIT_PATTERN = Pattern.compile("\\s+"); + + /** + * Note: These maps only contain lowercase keys/values, regarded as the + * {@link #toLowerCase(String) canonical form}. + * + *

This is the case for both extensions and MIME types. The mime.types + * data file contains examples of mixed-case MIME types, but some applications + * use the lowercase version of these same types. RFC 2045 section 2 states + * that MIME types are case insensitive. + */ + private final Map mMimeTypeToExtension; + private final Map mExtensionToMimeType; + + public MimeMapImpl(Map mimeTypeToExtension, + Map extensionToMimeType) { + this.mMimeTypeToExtension = new HashMap<>(mimeTypeToExtension); + for (Map.Entry entry : mimeTypeToExtension.entrySet()) { + checkValidMimeType(entry.getKey()); + checkValidExtension(entry.getValue()); + } + this.mExtensionToMimeType = new HashMap<>(extensionToMimeType); + for (Map.Entry entry : extensionToMimeType.entrySet()) { + checkValidExtension(entry.getKey()); + checkValidMimeType(entry.getValue()); + } + } + + private static void checkValidMimeType(String s) { + if (MimeMap.isNullOrEmpty(s) || !s.equals(MimeMap.toLowerCase(s))) { + throw new IllegalArgumentException("Invalid MIME type: " + s); + } + } + + private static void checkValidExtension(String s) { + if (MimeMap.isNullOrEmpty(s) || !s.equals(MimeMap.toLowerCase(s))) { + throw new IllegalArgumentException("Invalid extension: " + s); + } + } + + static MimeMapImpl parseFromResources(String... resourceNames) { + Map mimeTypeToExtension = new HashMap<>(); + Map extensionToMimeType = new HashMap<>(); + for (String resourceName : resourceNames) { + parseTypes(mimeTypeToExtension, extensionToMimeType, resourceName); + } + return new MimeMapImpl(mimeTypeToExtension, extensionToMimeType); + } + + /** + * An element of a *mime.types file: A MIME type or an extension, with an optional + * prefix of "?" (if not overriding an earlier value). + */ + private static class Element { + public final boolean keepExisting; + public final String s; + + Element(boolean keepExisting, String value) { + this.keepExisting = keepExisting; + this.s = toLowerCase(value); + if (value.isEmpty()) { + throw new IllegalArgumentException(); + } + } + + public String toString() { + return keepExisting ? ("?" + s) : s; + } + } + + private static String maybePut(Map map, Element keyElement, String value) { + if (keyElement.keepExisting) { + return map.putIfAbsent(keyElement.s, value); + } else { + return map.put(keyElement.s, value); + } + } + + private static void parseTypes(Map mimeTypeToExtension, + Map extensionToMimeType, String resource) { + try (BufferedReader r = new BufferedReader( + new InputStreamReader(MimeMapImpl.class.getResourceAsStream(resource)))) { + String line; + while ((line = r.readLine()) != null) { + int commentPos = line.indexOf('#'); + if (commentPos >= 0) { + line = line.substring(0, commentPos); + } + line = line.trim(); + // The first time a MIME type is encountered it is mapped to the first extension + // listed in its line. The first time an extension is encountered it is mapped + // to the MIME type. + // + // When encountering a previously seen MIME type or extension, then by default + // the later ones override earlier mappings (put() semantics); however if a MIME + // type or extension is prefixed with '?' then any earlier mapping _from_ that + // MIME type / extension is kept (putIfAbsent() semantics). + final String[] split = SPLIT_PATTERN.split(line); + if (split.length <= 1) { + // Need mimeType + at least one extension to make a mapping. + // "mime.types" files may also contain lines with just a mimeType without + // an extension but we skip them as they provide no mapping info. + continue; + } + List lineElements = new ArrayList<>(split.length); + for (String s : split) { + boolean keepExisting = s.startsWith("?"); + if (keepExisting) { + s = s.substring(1); + } + if (s.isEmpty()) { + throw new IllegalArgumentException("Invalid entry in '" + line + "'"); + } + lineElements.add(new Element(keepExisting, s)); + } + + // MIME type -> first extension (one mapping) + // This will override any earlier mapping from this MIME type to another + // extension, unless this MIME type was prefixed with '?'. + Element mimeElement = lineElements.get(0); + List extensionElements = lineElements.subList(1, lineElements.size()); + String firstExtension = extensionElements.get(0).s; + maybePut(mimeTypeToExtension, mimeElement, firstExtension); + + // extension -> MIME type (one or more mappings). + // This will override any earlier mapping from this extension to another + // MIME type, unless this extension was prefixed with '?'. + for (Element extensionElement : extensionElements) { + maybePut(extensionToMimeType, extensionElement, mimeElement.s); + } + } + } catch (IOException | RuntimeException e) { + throw new RuntimeException("Failed to parse " + resource, e); + } + } + + @Override + protected String guessExtensionFromLowerCaseMimeType(String mimeType) { + return mMimeTypeToExtension.get(mimeType); + } + + @Override + protected String guessMimeTypeFromLowerCaseExtension(String extension) { + return mExtensionToMimeType.get(extension); + } +} -- cgit v1.2.3