diff options
-rw-r--r-- | luni/src/main/java/libcore/net/MimeMap.java | 142 | ||||
-rw-r--r-- | luni/src/main/java/libcore/net/MimeMapImpl.java | 140 | ||||
-rw-r--r-- | luni/src/main/java/libcore/net/MimeUtils.java | 117 | ||||
-rw-r--r-- | luni/src/test/java/libcore/libcore/net/MimeMapTest.java | 134 | ||||
-rw-r--r-- | non_openjdk_java_files.bp | 2 |
5 files changed, 423 insertions, 112 deletions
diff --git a/luni/src/main/java/libcore/net/MimeMap.java b/luni/src/main/java/libcore/net/MimeMap.java new file mode 100644 index 0000000000..1db182414f --- /dev/null +++ b/luni/src/main/java/libcore/net/MimeMap.java @@ -0,0 +1,142 @@ +/* + * 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 libcore.net; + +import java.util.Locale; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import libcore.util.NonNull; +import libcore.util.Nullable; + +/** + * Maps from MIME types to file extensions and back. + * @hide + */ +public abstract class MimeMap { + private static AtomicReference<MimeMap> defaultHolder = new AtomicReference<>( + MimeMapImpl.parseFromResources("mime.types", "android.mime.types")); + + /** + * @return The system's current default {@link MimeMap}. + */ + public static @NonNull MimeMap getDefault() { + return defaultHolder.get(); + } + + /** + * Atomically sets the system's default {@link MimeMap} to be {@code update} if the + * current value {@code == expect}. + * + * @param expect the expected current default {@link MimeMap}; must not be null. + * @param update the new default {@link MimeMap} to set; must not be null. + * @return whether the update was successful. + */ + public static boolean compareAndSetDefault(@NonNull MimeMap expect, @NonNull MimeMap update) { + Objects.requireNonNull(expect); + Objects.requireNonNull(update); + return defaultHolder.compareAndSet(expect, update); + } + + /** + * Returns whether the given case insensitive extension has a registered MIME type. + * + * @param extension A file extension without the leading '.' + * @return Whether a MIME type has been registered for the given case insensitive file + * extension. + */ + public final boolean hasExtension(@Nullable String extension) { + return guessMimeTypeFromExtension(extension) != null; + } + + /** + * Returns the MIME type for the given case insensitive file extension. + * If {@code extension} is {@code null} or {@code ""}, then this method always returns + * {@code null}. Otherwise, it delegates to + * {@link #guessMimeTypeFromLowerCaseExtension(String)}. + * + * @param extension A file extension without the leading '.' + * @return The lower-case MIME type registered for the given case insensitive file extension, + * or null if there is none. + */ + public final @Nullable String guessMimeTypeFromExtension(@Nullable String extension) { + if (isNullOrEmpty(extension)) { + return null; + } + extension = toLowerCase(extension); + String result = guessMimeTypeFromLowerCaseExtension(extension); + if (result != null) { + result = toLowerCase(result); + } + return result; + } + + /** + * @param extension A non-null, non-empty, lowercase file extension. + * @return The MIME type registered for the given file extension, or null if there is none. + */ + protected abstract @Nullable String guessMimeTypeFromLowerCaseExtension( + @NonNull String extension); + + /** + * @param mimeType A MIME type (i.e. {@code "text/plain") + * @return Whether the given case insensitive MIME type is + * {@link #guessMimeTypeFromExtension(String) mapped} to a file extension. + */ + public final boolean hasMimeType(@Nullable String mimeType) { + return guessExtensionFromMimeType(mimeType) != null; + } + + /** + * Returns the registered extension for the given case insensitive MIME type. Note that some + * MIME types map to multiple extensions. This call will return the most + * common extension for the given MIME type. + * @param mimeType A MIME type (i.e. text/plain) + * @return The lower-case file extension (without the leading "." that has been registered for + * the given case insensitive MIME type, or null if there is none. + */ + public final @Nullable String guessExtensionFromMimeType(@Nullable String mimeType) { + if (isNullOrEmpty(mimeType)) { + return null; + } + mimeType = toLowerCase(mimeType); + String result = guessExtensionFromLowerCaseMimeType(mimeType); + if (result != null) { + result = toLowerCase(result); + } + return result; + } + + /** + * @param mimeType A non-null, non-empty, lowercase file extension. + * @return The file extension (without the leading ".") for the given mimeType, or null if + * there is none. + */ + protected abstract @Nullable String guessExtensionFromLowerCaseMimeType( + @NonNull String mimeType); + + /** + * Returns the canonical (lowercase) form of the given extension or MIME type. + */ + static @NonNull String toLowerCase(@NonNull String s) { + return s.toLowerCase(Locale.ROOT); + } + + static boolean isNullOrEmpty(@Nullable String s) { + return s == null || s.isEmpty(); + } + +} diff --git a/luni/src/main/java/libcore/net/MimeMapImpl.java b/luni/src/main/java/libcore/net/MimeMapImpl.java new file mode 100644 index 0000000000..086e5b9059 --- /dev/null +++ b/luni/src/main/java/libcore/net/MimeMapImpl.java @@ -0,0 +1,140 @@ +/* + * 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 libcore.net; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; + +class MimeMapImpl extends MimeMap { + + private static final Pattern splitPattern = Pattern.compile("\\s+"); + + /** + * Note: These maps only contain lowercase keys/values, regarded as the + * {@link #toLowerCase(String) canonical form}. + * + * <p>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<String, String> mimeTypeToExtension; + private final Map<String, String> extensionToMimeType; + + public MimeMapImpl(Map<String, String> mimeTypeToExtension, + Map<String, String> extensionToMimeType) { + this.mimeTypeToExtension = new HashMap<>(mimeTypeToExtension); + for (Map.Entry<String, String> entry : mimeTypeToExtension.entrySet()) { + checkValidMimeType(entry.getKey()); + checkValidExtension(entry.getValue()); + } + this.extensionToMimeType = new HashMap<>(extensionToMimeType); + for (Map.Entry<String, String> 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 (isNullOrEmpty(s) || !s.equals(toLowerCase(s))) { + throw new IllegalArgumentException("Invalid extension: " + s); + } + } + + static MimeMapImpl parseFromResources(String... resourceNames) { + Map<String, String> mimeTypeToExtension = new HashMap<>(); + Map<String, String> extensionToMimeType = new HashMap<>(); + for (String resourceName : resourceNames) { + parseTypes(mimeTypeToExtension, extensionToMimeType, resourceName); + } + return new MimeMapImpl(mimeTypeToExtension, extensionToMimeType); + } + + private static void parseTypes(Map<String, String> mimeTypeToExtension, + Map<String, String> extensionToMimeType, String resource) { + try (BufferedReader r = new BufferedReader( + new InputStreamReader(MimeMap.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(); + if (line.equals("")) { + continue; + } + + final String[] split = splitPattern.split(line); + final String mimeType = toLowerCase(split[0]); + if (isNullOrEmpty(mimeType)) { + throw new IllegalArgumentException( + "Invalid mimeType " + mimeType + " in: " + line); + } + for (int i = 1; i < split.length; i++) { + String extension = toLowerCase(split[i]); + if (isNullOrEmpty(extension)) { + throw new IllegalArgumentException( + "Invalid extension " + extension + " in: " + line); + } + + // Normally the first MIME type definition wins, and the + // last extension definition wins. However, a file can + // override a MIME type definition by adding the "!" suffix + // to an extension. + + if (extension.endsWith("!")) { + extension = extension.substring(0, extension.length() - 1); + + // Overriding MIME definition wins + mimeTypeToExtension.put(mimeType, extension); + } else { + // First MIME definition wins + if (!mimeTypeToExtension.containsKey(mimeType)) { + mimeTypeToExtension.put(mimeType, extension); + } + } + + // Last extension definition wins + extensionToMimeType.put(extension, mimeType); + } + } + } catch (IOException e) { + throw new RuntimeException("Failed to parse " + resource, e); + } + } + + @Override + protected String guessExtensionFromLowerCaseMimeType(String mimeType) { + return mimeTypeToExtension.get(mimeType); + } + + @Override + protected String guessMimeTypeFromLowerCaseExtension(String extension) { + return extensionToMimeType.get(extension); + } +} diff --git a/luni/src/main/java/libcore/net/MimeUtils.java b/luni/src/main/java/libcore/net/MimeUtils.java index 8cd0147c12..a9dd00a237 100644 --- a/luni/src/main/java/libcore/net/MimeUtils.java +++ b/luni/src/main/java/libcore/net/MimeUtils.java @@ -17,13 +17,7 @@ package libcore.net; import dalvik.annotation.compat.UnsupportedAppUsage; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.regex.Pattern; +import java.util.concurrent.atomic.AtomicReference; /** * Utilities for dealing with MIME types. @@ -33,105 +27,12 @@ import java.util.regex.Pattern; @libcore.api.CorePlatformApi public final class MimeUtils { - private static final Pattern splitPattern = Pattern.compile("\\s+"); - - /** - * Note: These maps only contain lowercase keys/values, regarded as the - * {@link #canonicalize(String) canonical form}. - * - * <p>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 static final Map<String, String> mimeTypeToExtensionMap = new HashMap<>(); - private static final Map<String, String> extensionToMimeTypeMap = new HashMap<>(); - - static { - parseTypes("mime.types"); - parseTypes("android.mime.types"); - } - - private static void parseTypes(String resource) { - try (BufferedReader r = new BufferedReader( - new InputStreamReader(MimeUtils.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(); - if (line.equals("")) { - continue; - } - - final String[] split = splitPattern.split(line); - final String mimeType = canonicalize(split[0]); - if (!allowedInMap(mimeType)) { - throw new IllegalArgumentException( - "Invalid mimeType " + mimeType + " in: " + line); - } - for (int i = 1; i < split.length; i++) { - String extension = canonicalize(split[i]); - if (!allowedInMap(extension)) { - throw new IllegalArgumentException( - "Invalid extension " + extension + " in: " + line); - } - - // Normally the first MIME type definition wins, and the - // last extension definition wins. However, a file can - // override a MIME type definition by adding the "!" suffix - // to an extension. - - if (extension.endsWith("!")) { - extension = extension.substring(0, extension.length() - 1); - - // Overriding MIME definition wins - mimeTypeToExtensionMap.put(mimeType, extension); - } else { - // First MIME definition wins - if (!mimeTypeToExtensionMap.containsKey(mimeType)) { - mimeTypeToExtensionMap.put(mimeType, extension); - } - } - - // Last extension definition wins - extensionToMimeTypeMap.put(extension, mimeType); - } - } - } catch (IOException e) { - throw new RuntimeException("Failed to parse " + resource, e); - } - } - private MimeUtils() { } - /** - * Returns the canonical (lowercase) form of the given extension or MIME type. - */ - private static String canonicalize(String s) { - return s.toLowerCase(Locale.ROOT); - } - - /** - * Checks whether the given extension or MIME type might be valid and - * therefore may appear in the mimeType <-> extension maps. - */ - private static boolean allowedInMap(String s) { - return s != null && !s.isEmpty(); - } - - /** - * Returns true if the given case insensitive MIME type has an entry in the map. - * @param mimeType A MIME type (i.e. text/plain) - * @return True if a extension has been registered for - * the given case insensitive MIME type. - */ @libcore.api.CorePlatformApi public static boolean hasMimeType(String mimeType) { - return (guessExtensionFromMimeType(mimeType) != null); + return MimeMap.getDefault().hasMimeType(mimeType); } /** @@ -143,11 +44,7 @@ public final class MimeUtils { @UnsupportedAppUsage @libcore.api.CorePlatformApi public static String guessMimeTypeFromExtension(String extension) { - if (!allowedInMap(extension)) { - return null; - } - extension = canonicalize(extension); - return extensionToMimeTypeMap.get(extension); + return MimeMap.getDefault().guessMimeTypeFromExtension(extension); } /** @@ -158,7 +55,7 @@ public final class MimeUtils { */ @libcore.api.CorePlatformApi public static boolean hasExtension(String extension) { - return (guessMimeTypeFromExtension(extension) != null); + return MimeMap.getDefault().hasExtension(extension); } /** @@ -172,10 +69,6 @@ public final class MimeUtils { @UnsupportedAppUsage @libcore.api.CorePlatformApi public static String guessExtensionFromMimeType(String mimeType) { - if (!allowedInMap(mimeType)) { - return null; - } - mimeType = canonicalize(mimeType); - return mimeTypeToExtensionMap.get(mimeType); + return MimeMap.getDefault().guessExtensionFromMimeType(mimeType); } } diff --git a/luni/src/test/java/libcore/libcore/net/MimeMapTest.java b/luni/src/test/java/libcore/libcore/net/MimeMapTest.java new file mode 100644 index 0000000000..59980929d2 --- /dev/null +++ b/luni/src/test/java/libcore/libcore/net/MimeMapTest.java @@ -0,0 +1,134 @@ +/* + * 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 libcore.libcore.net; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; + +import libcore.net.MimeMap; +import libcore.util.NonNull; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; + +public class MimeMapTest { + + /** Exposes {@link MimeMap}'s protected methods publicly so that mock calls can be verified. */ + public static abstract class TestMimeMap extends MimeMap { + @Override + public abstract String guessMimeTypeFromLowerCaseExtension(@NonNull String extension); + + @Override + public abstract String guessExtensionFromLowerCaseMimeType(@NonNull String mimeType); + } + + private TestMimeMap mimeMap; + + + @Before public void setUp() { + mimeMap = mock(TestMimeMap.class); + } + + @After public void tearDown() { + mimeMap = null; + } + + @Test public void invalidExtension() { + assertNull(mimeMap.guessMimeTypeFromExtension(null)); + assertNull(mimeMap.guessMimeTypeFromExtension("")); + assertFalse(mimeMap.hasExtension(null)); + assertFalse(mimeMap.hasExtension("")); + + verify(mimeMap, never()).guessExtensionFromLowerCaseMimeType(anyString()); + verify(mimeMap, never()).guessMimeTypeFromLowerCaseExtension(anyString()); + + } + + @Test public void invalidMimeType() { + assertNull(mimeMap.guessExtensionFromMimeType(null)); + assertNull(mimeMap.guessExtensionFromMimeType("")); + assertFalse(mimeMap.hasMimeType(null)); + assertFalse(mimeMap.hasMimeType("")); + + verify(mimeMap, never()).guessExtensionFromLowerCaseMimeType(anyString()); + verify(mimeMap, never()).guessMimeTypeFromLowerCaseExtension(anyString()); + } + + @Test public void caseNormalization() { + when(mimeMap.guessExtensionFromLowerCaseMimeType("application/msword")).thenReturn("DoC"); + when(mimeMap.guessMimeTypeFromLowerCaseExtension("doc")).thenReturn("APPLication/msWORD"); + + assertEquals("application/msword", mimeMap.guessMimeTypeFromExtension("dOc")); + assertEquals("doc", mimeMap.guessExtensionFromMimeType("appliCATion/mSWOrd")); + } + + @Test public void unmapped() { + assertNull(mimeMap.guessExtensionFromMimeType("test/mime")); + assertFalse(mimeMap.hasMimeType("test/mime")); + + assertNull(mimeMap.guessMimeTypeFromExtension("test")); + assertFalse(mimeMap.hasExtension("test")); + + verify(mimeMap, times(2)).guessExtensionFromLowerCaseMimeType("test/mime"); + verify(mimeMap, times(2)).guessMimeTypeFromLowerCaseExtension("test"); + } + + @Test public void compareAndSetDefault() { + MimeMap otherMimeMap = mock(TestMimeMap.class); + MimeMap defaultMimeMap = MimeMap.getDefault(); + assertTrue(MimeMap.compareAndSetDefault(defaultMimeMap, mimeMap)); + try { + assertNotNull(defaultMimeMap); + assertEquals(mimeMap, MimeMap.getDefault()); + assertFalse(MimeMap.compareAndSetDefault(defaultMimeMap, otherMimeMap)); + } finally { + assertTrue(MimeMap.compareAndSetDefault(mimeMap, defaultMimeMap)); + } + } + + @Test public void compareAndSetDefault_null() { + MimeMap defaultMimeMap = MimeMap.getDefault(); + try { + MimeMap.compareAndSetDefault(defaultMimeMap, null); + fail(); + } catch (NullPointerException expected) { + } + + try { + MimeMap.compareAndSetDefault(null, defaultMimeMap); + fail(); + } catch (NullPointerException expected) { + } + + // For comparison, this does not throw (but has no effect): + MimeMap.compareAndSetDefault(defaultMimeMap, defaultMimeMap); + assertEquals(defaultMimeMap, MimeMap.getDefault()); + } + +} diff --git a/non_openjdk_java_files.bp b/non_openjdk_java_files.bp index 3e4083fdef..9628017ad0 100644 --- a/non_openjdk_java_files.bp +++ b/non_openjdk_java_files.bp @@ -176,6 +176,8 @@ filegroup { "luni/src/main/java/libcore/io/Os.java", "luni/src/main/java/libcore/io/Streams.java", "luni/src/main/java/libcore/net/InetAddressUtils.java", + "luni/src/main/java/libcore/net/MimeMap.java", + "luni/src/main/java/libcore/net/MimeMapImpl.java", "luni/src/main/java/libcore/net/MimeUtils.java", "luni/src/main/java/libcore/net/NetworkSecurityPolicy.java", "luni/src/main/java/libcore/net/event/NetworkEventDispatcher.java", |