diff options
author | Tobias Thierer <tobiast@google.com> | 2018-02-08 21:45:09 +0000 |
---|---|---|
committer | Tobias Thierer <tobiast@google.com> | 2018-02-13 12:20:13 +0000 |
commit | 7dbee73c44ca7965472ae92456a3c131ce170adc (patch) | |
tree | 5320f5ee467d645da902bc557709ba1ad1d17285 | |
parent | 0692a133e3895e44eb828ca1298e057bed7ed731 (diff) |
Update java.net.URL to OpenJDK 8u121-b13.
This CL integrates upstream commit (a behavior change)
http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/1837db2935fd
and completes the update to OpenJDK 8u121-b13.
Notes:
- Android's URL.hashCode is not part of the serialization.
The Android change for this used to be at the place where
the field was declared, but is now in the logic associated
with readObject(); UrlDeserializedState's hashCode field
is kept, but its value is always -1; the existing test
URLTest.testUrlSerializationWithHashCode() checks that the
hashCode is recomputed during deserialization.
- The upstream commit introduces a behavior change around
deserialization of URLs which inconsistent state (eg.
host and port disagreeing with authority); this CL applies
the same behavior change to Android and adds test coverage
for the new behavior.
- The behavior change differs between URLs handled by built-in
vs. custom (application) URLStreamHandler implementation.
Android makes the same distinction, although the method
isBuiltinStreamHandler() that distinguishes built-in handler
implementations was adjusted to Android's needs.
- For built-in Handlers, readResolve() produces a new URL
object via its String representation. This means that,
for example, host and port are recomputed from the authority;
similarly, the split between host and file parts is recomputed.
This guards against inconsistencies of those field values in
the serialized form.
- For custom Handler implementations, a more conservative approach
is taken; a null authority is recomputed from port and nonempty
host, but host/port consistency with authority is not otherwise
checked. Android is adopting this behavior in this CL and adding
test coverage, but the rationale behind the exact behavior choices
was not analyzed in detail.
- This CL factors the Android-added createBuiltinHandler() out into
a helper method in order to put it adjacent to
createBuiltinHandlerClassNames(), which must remain consistent
(tested by testCommonProtocolsAreHandledByBuiltinHandlers()).
Bug: 71861693
Bug: 35910877
Test: CtsLibcoreTestCases
Change-Id: Ieffa8df1818b8b31601214e74da617a3e2e78c2c
-rw-r--r-- | luni/src/test/java/libcore/java/net/URLTest.java | 179 | ||||
-rw-r--r-- | luni/src/test/java/libcore/libcore/util/SerializationTester.java | 2 | ||||
-rw-r--r-- | ojluni/src/main/java/java/net/URL.java | 315 |
3 files changed, 461 insertions, 35 deletions
diff --git a/luni/src/test/java/libcore/java/net/URLTest.java b/luni/src/test/java/libcore/java/net/URLTest.java index 4bae646e65..1ff9b336cb 100644 --- a/luni/src/test/java/libcore/java/net/URLTest.java +++ b/luni/src/test/java/libcore/java/net/URLTest.java @@ -16,14 +16,28 @@ package libcore.java.net; +import junit.framework.TestCase; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.net.Inet6Address; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.net.URLStreamHandlerFactory; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.Callable; +import libcore.libcore.util.SerializationTester; import dalvik.system.BlockGuard; -import junit.framework.TestCase; -import libcore.libcore.util.SerializationTester; + +import static java.util.Arrays.asList; public final class URLTest extends TestCase { @@ -109,6 +123,149 @@ public final class URLTest extends TestCase { } /** + * For a custom URLStreamHandler, a (de)serialization round trip reconstructs an + * inconsistently null authority from host and port. + */ + public void testUrlSerializationRoundTrip_customHandler_nullAuthorityReconstructed() + throws Exception { + withCustomURLStreamHandlerFactory(() -> { + URL url = new URL("android://example.com:80/file"); + getUrlField("authority").set(url, null); + URL reserializedUrl = (URL) SerializationTester.reserialize(url); + + assertFields(url, "android", "example.com", 80, null, "/file"); + assertFields(reserializedUrl, "android", "example.com", 80, "example.com:80", "/file"); + return null; + }); + } + + /** + * For a custom URLStreamHandler, a (de)serialization round trip does not reconstruct + * an inconsistent but nonnull authority from host and port. + */ + public void testUrlSerializationRoundTrip_customHandler_nonnullAuthorityNotReconstructed() + throws Exception { + withCustomURLStreamHandlerFactory(() -> { + URL url = new URL("android://example.com/file"); + getUrlField("authority").set(url, "evil.com:1234"); + URL reserializedUrl = (URL) SerializationTester.reserialize(url); + + assertFields(url, "android", "example.com", -1, "evil.com:1234", "/file"); + assertFields(reserializedUrl, "android", "example.com", -1, "evil.com:1234", "/file"); + return null; + }); + } + + /** + * For a custom URLStreamHandler, a (de)serialization round trip does not + * reconstruct host and port from the authority, even if host is null. + */ + public void testUrlSerializationRoundTrip_customHandler_hostAndPortNotReconstructed() + throws Exception { + checkUrlSerializationRoundTrip_customHandler_hostAndPortNotReconstructed(null /* host */); + checkUrlSerializationRoundTrip_customHandler_hostAndPortNotReconstructed("evil.com"); + } + + private void checkUrlSerializationRoundTrip_customHandler_hostAndPortNotReconstructed( + final String hostOrNull) throws Exception { + withCustomURLStreamHandlerFactory(() -> { + URL url = new URL("android://example.com:80/file"); + getUrlField("host").set(url, hostOrNull); + getUrlField("port").set(url, 12345); + URL reserializedUrl = (URL) SerializationTester.reserialize(url); + + assertFields(url, "android", hostOrNull, 12345, "example.com:80", "/file"); + assertFields(reserializedUrl, "android", hostOrNull, 12345, "example.com:80", "/file"); + return null; + }); + } + + /** + * Temporarily registers a {@link URLStreamHandlerFactory} that accepts any protocol, + * and then, while that factory is registered, runs the given {@code callable} on this + * thread. + * @throw Exception any Exception thrown by the Callable will be thrown on to the caller. + */ + private static void withCustomURLStreamHandlerFactory(Callable<Void> callable) + throws Exception { + Field factoryField = getUrlField("factory"); + assertTrue(Modifier.isStatic(factoryField.getModifiers())); + URLStreamHandlerFactory oldFactory = (URLStreamHandlerFactory) factoryField.get(null); + try { + URL.setURLStreamHandlerFactory( + protocol -> new libcore.java.net.customstreamhandler.http.Handler()); + callable.call(); + } finally { + factoryField.set(null, null); + URL.setURLStreamHandlerFactory(oldFactory); + } + } + + /** + * Host and port are reconstructed from the authority during deserialization. + */ + public void testUrlSerializationRoundTrip_builtinHandler_hostAndPortReconstructed() + throws Exception { + URL url = new URL("http://example.com:42/file"); + getUrlField("host").set(url, "wronghost.com"); + getUrlField("port").setInt(url, 1234); + URL reserializedUrl = (URL) SerializationTester.reserialize(url); + assertFields(url, "http", "wronghost.com", 1234, "example.com:42", "/file"); + assertFields(reserializedUrl, "http", "example.com", 42, "example.com:42", "/file"); + + // Check that the normalization occurs during deserialization rather than during + // serialization. + assertFalse(Arrays.equals( + SerializationTester.serialize(url), + SerializationTester.serialize(reserializedUrl) + )); + assertTrue(Arrays.equals( + SerializationTester.serialize(reserializedUrl), + SerializationTester.serialize(reserializedUrl) + )); + } + + /** + * The authority is not reconstructed from host and port, but the other way around. + */ + public void testUrlSerializationRoundTrip_builtinHandler_authorityNotReconstructed() + throws Exception { + URL url = new URL("http://example.com/file"); + getUrlField("authority").set(url, "newhost.com:80"); + URL reserializedUrl = (URL) SerializationTester.reserialize(url); + + assertFields(url, "http", "example.com", -1, "newhost.com:80", "/file"); + assertFields(reserializedUrl, "http", "newhost.com", 80, "newhost.com:80", "/file"); + } + + /** + * The boundary where the authority part ends and the file part starts is + * reconstructed during deserialization. + */ + public void testUrlSerializationRoundTrip_builtinHandler_authorityAndFileReconstructed() + throws Exception { + URL url = new URL("http://temporaryhost.com/temporaryfile"); + getUrlField("authority").set(url, "exam"); + getUrlField("file").set(url, "ple.com:80/file"); + URL reserializedUrl = (URL) SerializationTester.reserialize(url); + assertFields(reserializedUrl, "http", "example.com", 80, "example.com:80", "/file"); + } + + private static Field getUrlField(String fieldName) throws Exception { + Field result = URL.class.getDeclaredField(fieldName); + result.setAccessible(true); + return result; + } + + private static void assertFields(URL url, + String protocol, String host, int port, String authority, String file) { + assertEquals( + asList(protocol, host, port, authority, file), + asList(url.getProtocol(), url.getHost(), url.getPort(), url.getAuthority(), + url.getFile())); + } + + /** * The serialized form of a URL includes its hash code. But the hash code * is not documented. Check that we don't return a deserialized hash code * from a deserialized value. @@ -293,6 +450,24 @@ public final class URLTest extends TestCase { assertEquals("query@at", url.getQuery()); } + public void testCommonProtocolsAreHandledByBuiltinHandlers() throws Exception { + Method getURLStreamHandler = URL.class.getDeclaredMethod( + "getURLStreamHandler", String.class); + getURLStreamHandler.setAccessible(true); + Set<String> builtinHandlers = + (Set<String>) getUrlField("BUILTIN_HANDLER_CLASS_NAMES").get(null); + Set<String> commonHandlers = new HashSet<>(); + for (String protocol : Arrays.asList("file", "ftp", "jar", "http", "https")) { + URLStreamHandler handler = + (URLStreamHandler) getURLStreamHandler.invoke(null, protocol); + assertNotNull("Handler for protocol " + protocol + " should exist", handler); + commonHandlers.add(handler.getClass().getName()); + } + assertTrue("Built-in handlers " + builtinHandlers + " should contain all of the handlers " + + commonHandlers + " for common protocols.", + builtinHandlers.containsAll(commonHandlers)); + } + public void testColonInQuery() throws Exception { URL url = new URL("http://host/file?query:colon"); assertEquals("/file?query:colon", url.getFile()); diff --git a/luni/src/test/java/libcore/libcore/util/SerializationTester.java b/luni/src/test/java/libcore/libcore/util/SerializationTester.java index d30baf52cf..48a58b4fd0 100644 --- a/luni/src/test/java/libcore/libcore/util/SerializationTester.java +++ b/luni/src/test/java/libcore/libcore/util/SerializationTester.java @@ -81,7 +81,7 @@ public class SerializationTester<T> { } } - private static byte[] serialize(Object object) throws IOException { + public static byte[] serialize(Object object) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); new ObjectOutputStream(out).writeObject(object); return out.toByteArray(); diff --git a/ojluni/src/main/java/java/net/URL.java b/ojluni/src/main/java/java/net/URL.java index 495cc190ee..74dec92246 100644 --- a/ojluni/src/main/java/java/net/URL.java +++ b/ojluni/src/main/java/java/net/URL.java @@ -1,6 +1,6 @@ /* * Copyright (C) 2014 The Android Open Source Project - * Copyright (c) 1995, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1995, 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,7 +28,14 @@ package java.net; import java.io.IOException; import java.io.InputStream; +import java.io.InvalidObjectException; +import java.io.ObjectInputStream.GetField; +import java.io.ObjectStreamException; +import java.io.ObjectStreamField; +import java.util.Collections; +import java.util.HashSet; import java.util.Hashtable; +import java.util.Set; import java.util.StringTokenizer; import sun.security.util.SecurityConstants; @@ -136,6 +143,9 @@ import sun.security.util.SecurityConstants; */ public final class URL implements java.io.Serializable { + // Android-changed: Custom built-in URLStreamHandlers for http, https. + // static final String BUILTIN_HANDLERS_PREFIX = "sun.net.www.protocol"; + private static final Set<String> BUILTIN_HANDLER_CLASS_NAMES = createBuiltinHandlerClassNames(); static final long serialVersionUID = -7627629688361524110L; /** @@ -218,9 +228,9 @@ public final class URL implements java.io.Serializable { /* Our hash code. * @serial */ - // Android-changed: App compat. The cache of hash code should not be serialized. - //private int hashCode = -1; - private transient int hashCode = -1; + private int hashCode = -1; + + private transient UrlDeserializedState tempState; /** * Creates a {@code URL} object from the specified @@ -1178,7 +1188,9 @@ public final class URL implements java.io.Serializable { packagePrefixList += "sun.net.www.protocol"; */ final String packagePrefixList = System.getProperty(protocolPathProp,""); - StringTokenizer packagePrefixIter = new StringTokenizer(packagePrefixList, "|"); + + StringTokenizer packagePrefixIter = + new StringTokenizer(packagePrefixList, "|"); while (handler == null && packagePrefixIter.hasMoreTokens()) { @@ -1210,31 +1222,16 @@ public final class URL implements java.io.Serializable { } } - // BEGIN Android-added: Makes okhttp the default http/https handler. + // BEGIN Android-added: Custom built-in URLStreamHandlers for http, https. // Fallback to built-in stream handler. if (handler == null) { try { - // Use of okhttp for http and https - // Removed unnecessary use of reflection for sun classes - if (protocol.equals("file")) { - handler = new sun.net.www.protocol.file.Handler(); - } else if (protocol.equals("ftp")) { - handler = new sun.net.www.protocol.ftp.Handler(); - } else if (protocol.equals("jar")) { - handler = new sun.net.www.protocol.jar.Handler(); - } else if (protocol.equals("http")) { - handler = (URLStreamHandler)Class. - forName("com.android.okhttp.HttpHandler").newInstance(); - } else if (protocol.equals("https")) { - handler = (URLStreamHandler)Class. - forName("com.android.okhttp.HttpsHandler").newInstance(); - } - // END Android-changed + handler = createBuiltinHandler(protocol); } catch (Exception e) { throw new AssertionError(e); } } - // END Android-added: Makes okhttp the default http/https handler. + // END Android-added: Custom built-in URLStreamHandlers for http, https. synchronized (streamHandlerLock) { @@ -1273,6 +1270,69 @@ public final class URL implements java.io.Serializable { } + // BEGIN Android-added: Custom built-in URLStreamHandlers for http, https. + /** + * Returns an instance of the built-in handler for the given protocol, or null if none exists. + */ + private static URLStreamHandler createBuiltinHandler(String protocol) + throws ClassNotFoundException, InstantiationException, IllegalAccessException { + URLStreamHandler handler = null; + if (protocol.equals("file")) { + handler = new sun.net.www.protocol.file.Handler(); + } else if (protocol.equals("ftp")) { + handler = new sun.net.www.protocol.ftp.Handler(); + } else if (protocol.equals("jar")) { + handler = new sun.net.www.protocol.jar.Handler(); + } else if (protocol.equals("http")) { + handler = (URLStreamHandler)Class. + forName("com.android.okhttp.HttpHandler").newInstance(); + } else if (protocol.equals("https")) { + handler = (URLStreamHandler)Class. + forName("com.android.okhttp.HttpsHandler").newInstance(); + } + return handler; + } + + /** Names of implementation classes returned by {@link #createBuiltinHandler(String)}. */ + private static Set<String> createBuiltinHandlerClassNames() { + Set<String> result = new HashSet<>(); + // Refer to class names rather than classes to avoid needlessly triggering <clinit>. + result.add("sun.net.www.protocol.file.Handler"); + result.add("sun.net.www.protocol.ftp.Handler"); + result.add("sun.net.www.protocol.jar.Handler"); + result.add("com.android.okhttp.HttpHandler"); + result.add("com.android.okhttp.HttpsHandler"); + return Collections.unmodifiableSet(result); + } + // END Android-added: Custom built-in URLStreamHandlers for http, https. + + /** + * @serialField protocol String + * + * @serialField host String + * + * @serialField port int + * + * @serialField authority String + * + * @serialField file String + * + * @serialField ref String + * + * @serialField hashCode int + * + */ + private static final ObjectStreamField[] serialPersistentFields = { + new ObjectStreamField("protocol", String.class), + new ObjectStreamField("host", String.class), + new ObjectStreamField("port", int.class), + new ObjectStreamField("authority", String.class), + new ObjectStreamField("file", String.class), + new ObjectStreamField("ref", String.class), + // Android-changed: App compat: hashCode should not be serialized. + // new ObjectStreamField("hashCode", int.class), }; + }; + /** * WriteObject is called to save the state of the URL to an * ObjectOutputStream. The handler is not saved since it is @@ -1295,16 +1355,69 @@ public final class URL implements java.io.Serializable { * stream handler. */ private synchronized void readObject(java.io.ObjectInputStream s) - throws IOException, ClassNotFoundException - { - s.defaultReadObject(); // read the fields - if ((handler = getURLStreamHandler(protocol)) == null) { + throws IOException, ClassNotFoundException { + GetField gf = s.readFields(); + String protocol = (String)gf.get("protocol", null); + if (getURLStreamHandler(protocol) == null) { throw new IOException("unknown protocol: " + protocol); } + String host = (String)gf.get("host", null); + int port = gf.get("port", -1); + String authority = (String)gf.get("authority", null); + String file = (String)gf.get("file", null); + String ref = (String)gf.get("ref", null); + // Android-changed: App compat: hashCode should not be serialized. + // int hashCode = gf.get("hashCode", -1); + final int hashCode = -1; + if (authority == null + && ((host != null && host.length() > 0) || port != -1)) { + if (host == null) + host = ""; + authority = (port == -1) ? host : host + ":" + port; + } + tempState = new UrlDeserializedState(protocol, host, port, authority, + file, ref, hashCode); + } + + /** + * Replaces the de-serialized object with an URL object. + * + * @return a newly created object from the deserialzed state. + * + * @throws ObjectStreamException if a new object replacing this + * object could not be created + */ + + private Object readResolve() throws ObjectStreamException { + + URLStreamHandler handler = null; + // already been checked in readObject + handler = getURLStreamHandler(tempState.getProtocol()); + + URL replacementURL = null; + if (isBuiltinStreamHandler(handler.getClass().getName())) { + replacementURL = fabricateNewURL(); + } else { + replacementURL = setDeserializedFields(handler); + } + return replacementURL; + } + + private URL setDeserializedFields(URLStreamHandler handler) { + URL replacementURL; + String userInfo = null; + String protocol = tempState.getProtocol(); + String host = tempState.getHost(); + int port = tempState.getPort(); + String authority = tempState.getAuthority(); + String file = tempState.getFile(); + String ref = tempState.getRef(); + int hashCode = tempState.getHashCode(); + // Construct authority part - if (authority == null && - ((host != null && host.length() > 0) || port != -1)) { + if (authority == null + && ((host != null && host.length() > 0) || port != -1)) { if (host == null) host = ""; authority = (port == -1) ? host : host + ":" + port; @@ -1323,8 +1436,8 @@ public final class URL implements java.io.Serializable { } // Construct path and query part - path = null; - query = null; + String path = null; + String query = null; if (file != null) { // Fix: only do this if hierarchical? int q = file.lastIndexOf('?'); @@ -1334,7 +1447,66 @@ public final class URL implements java.io.Serializable { } else path = file; } - hashCode = -1; + + // Set the object fields. + this.protocol = protocol; + this.host = host; + this.port = port; + this.file = file; + this.authority = authority; + this.ref = ref; + this.hashCode = hashCode; + this.handler = handler; + this.query = query; + this.path = path; + this.userInfo = userInfo; + replacementURL = this; + return replacementURL; + } + + private URL fabricateNewURL() + throws InvalidObjectException { + // create URL string from deserialized object + URL replacementURL = null; + String urlString = tempState.reconstituteUrlString(); + + try { + replacementURL = new URL(urlString); + } catch (MalformedURLException mEx) { + resetState(); + InvalidObjectException invoEx = new InvalidObjectException( + "Malformed URL: " + urlString); + invoEx.initCause(mEx); + throw invoEx; + } + replacementURL.setSerializedHashCode(tempState.getHashCode()); + resetState(); + return replacementURL; + } + + private boolean isBuiltinStreamHandler(String handlerClassName) { + // Android-changed: Some built-in handlers (eg. HttpHandler) are not in sun.net.www.protocol. + // return (handlerClassName.startsWith(BUILTIN_HANDLERS_PREFIX)); + return BUILTIN_HANDLER_CLASS_NAMES.contains(handlerClassName); + } + + private void resetState() { + this.protocol = null; + this.host = null; + this.port = -1; + this.file = null; + this.authority = null; + this.ref = null; + this.hashCode = -1; + this.handler = null; + this.query = null; + this.path = null; + this.userInfo = null; + this.tempState = null; + } + + private void setSerializedHashCode(int hc) { + this.hashCode = hc; } } @@ -1374,3 +1546,82 @@ class Parts { return ref; } } + +final class UrlDeserializedState { + private final String protocol; + private final String host; + private final int port; + private final String authority; + private final String file; + private final String ref; + private final int hashCode; + + public UrlDeserializedState(String protocol, + String host, int port, + String authority, String file, + String ref, int hashCode) { + this.protocol = protocol; + this.host = host; + this.port = port; + this.authority = authority; + this.file = file; + this.ref = ref; + this.hashCode = hashCode; + } + + String getProtocol() { + return protocol; + } + + String getHost() { + return host; + } + + String getAuthority () { + return authority; + } + + int getPort() { + return port; + } + + String getFile () { + return file; + } + + String getRef () { + return ref; + } + + int getHashCode () { + return hashCode; + } + + String reconstituteUrlString() { + + // pre-compute length of StringBuilder + int len = protocol.length() + 1; + if (authority != null && authority.length() > 0) + len += 2 + authority.length(); + if (file != null) { + len += file.length(); + } + if (ref != null) + len += 1 + ref.length(); + StringBuilder result = new StringBuilder(len); + result.append(protocol); + result.append(":"); + if (authority != null && authority.length() > 0) { + result.append("//"); + result.append(authority); + } + if (file != null) { + result.append(file); + } + if (ref != null) { + result.append("#"); + result.append(ref); + } + return result.toString(); + } +} |