summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--luni/src/test/java/libcore/java/net/URLTest.java179
-rw-r--r--luni/src/test/java/libcore/libcore/util/SerializationTester.java2
-rw-r--r--ojluni/src/main/java/java/net/URL.java315
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();
+ }
+}