jdk-24/test/jdk/java/net/URL/EarlyOrDelayedParsing.java

508 lines
19 KiB
Java

/*
* Copyright (c) 2022, 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
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
* @test
* @bug 8293590
* @summary URL built-in protocol handlers should parse the URL early
* to avoid constructing URLs for which openConnection
* would later throw an exception, when possible.
* A jdk.net.url.delayParsing property allows to switch that
* behavior off to mitigate risks of regression
* @run junit EarlyOrDelayedParsing
* @run junit/othervm -Djdk.net.url.delayParsing EarlyOrDelayedParsing
* @run junit/othervm -Djdk.net.url.delayParsing=true EarlyOrDelayedParsing
* @run junit/othervm -Djdk.net.url.delayParsing=false EarlyOrDelayedParsing
*/
import java.io.IOException;
import java.net.ConnectException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static java.lang.System.err;
import static org.junit.jupiter.api.Assertions.*;
public class EarlyOrDelayedParsing {
public final boolean EARLY_PARSING;
{
String value = System.getProperty("jdk.net.url.delayParsing", "false");
EARLY_PARSING = !value.isEmpty() && !Boolean.parseBoolean(value);
}
// Some characters that when included at the wrong place
// in the authority component, without being escaped, would
// cause an exception.
private static final String EXCLUDED_DELIMS = "<>\" ";
private static final String UNWISE = "{}|\\^`";
private static final String DELIMS = "[]/?#@";
// Test data used to test exceptions thrown by URL
// at some point, when constructed with some illegal input.
sealed interface URLArgTest
permits OneArgTest, TwoArgsTest, ThreeArgsTest, FourArgsTest {
// Some character that is expected to cause an exception
// at some point, and which this test case is built for
int character();
// An URL string containing the illegal character
String url();
// Some characters are already checked at construction
// time. They will cause an exception to be thrown,
// whether delayed parsing is activated or not.
// This method returns true if an exception is
// expected at construction time for this test case,
// even when delayed parsing is activated.
boolean early(int c);
// The URL scheme this test case is built for.
// Typically, one of "http", "https", "ftp"...
default String scheme() {
return scheme(url());
}
// Return the URL string of this test case, after
// substituting its scheme with the given scheme.
default String urlWithScheme(String scheme) {
String url = url();
int colon = url.indexOf(':');
String urlWithScheme = scheme + url.substring(colon);
return urlWithScheme;
}
// Which exception to expect when parsing is delayed
default boolean acceptDelayedException(Throwable exception) {
return exception instanceof MalformedURLException
|| exception instanceof UnknownHostException;
}
default String describe() {
return this.getClass().getSimpleName() + "(url=" + url() + ")";
}
static int port(String protocol) {
return switch (protocol) {
case "http" -> 80;
case "https" -> 443;
case "ftp" -> 21;
default -> -1;
};
}
static String scheme(String url) {
return url.substring(0, url.indexOf(':'));
}
}
// Test data for the one arg constructor
// public URL(String spec) throws MalformedURLException
sealed interface OneArgTest extends URLArgTest {
// Create a new test case identical to this one but
// with a different URL scheme
default OneArgTest withScheme(String scheme) {
String urlWithScheme = urlWithScheme(scheme);
if (this instanceof OfHost) {
return new OfHost(character(), urlWithScheme);
}
if (this instanceof OfUserInfo) {
return new OfUserInfo(character(), urlWithScheme);
}
throw new AssertionError("unexpected subclass: " + this.getClass());
}
@Override
default boolean early(int c) {
return this instanceof OfHost &&
(c < 31 || c == 127);
}
@Override
default boolean acceptDelayedException(Throwable exception) {
return URLArgTest.super.acceptDelayedException(exception)
|| "file".equalsIgnoreCase(scheme())
&& character() == '\\'
&& exception instanceof IOException;
}
record OfHost(int character, String url) implements OneArgTest { }
record OfUserInfo(int character, String url) implements OneArgTest { }
static OneArgTest ofHost(int c) {
return new OfHost(c, "http://local%shost/".formatted(Character.toString(c)));
}
static OneArgTest ofUserInfo(int c) {
return new OfUserInfo(c, "http://user%sinfo@localhost:9999/".formatted(Character.toString(c)));
}
}
// Test data for the two arg constructor
// public URL(URL context, String spec) throws MalformedURLException
sealed interface TwoArgsTest extends URLArgTest {
// Create a new test case identical to this one but
// with a different URL scheme
default TwoArgsTest withScheme(String scheme) {
String urlWithScheme = urlWithScheme(scheme);
if (this instanceof OfTwoArgsHost) {
return new OfTwoArgsHost(character(), urlWithScheme);
}
if (this instanceof OfTwoArgsUserInfo) {
return new OfTwoArgsUserInfo(character(), urlWithScheme);
}
throw new AssertionError("unexpected subclass: " + this.getClass());
}
@Override
default boolean early(int c) {
return this instanceof OfTwoArgsHost &&
(c < 31 || c == 127);
}
@Override
default boolean acceptDelayedException(Throwable exception) {
return URLArgTest.super.acceptDelayedException(exception)
|| "file".equalsIgnoreCase(scheme())
&& character() == '\\'
&& exception instanceof IOException;
}
record OfTwoArgsHost(int character, String url) implements TwoArgsTest { }
record OfTwoArgsUserInfo(int character, String url) implements TwoArgsTest { }
static TwoArgsTest ofHost(int c) {
return new OfTwoArgsHost(c, "http://local%shost/".formatted(Character.toString(c)));
}
static TwoArgsTest ofUserInfo(int c) {
return new OfTwoArgsUserInfo(c, "http://user%sinfo@localhost:9999/".formatted(Character.toString(c)));
}
static TwoArgsTest ofOneArgTest(OneArgTest test) {
if (test instanceof OneArgTest.OfHost) {
return ofHost(test.character());
} else if (test instanceof OneArgTest.OfUserInfo) {
return ofUserInfo(test.character());
}
throw new AssertionError("can't convert to TwoArgsTest: "
+ test.getClass());
}
}
// Test data for the three args constructor
// public URL(String scheme, String host, String file)
// throws MalformedURLException
sealed interface ThreeArgsTest extends URLArgTest {
// the host component
String host();
// the path + query components
String file();
// Create a new test case identical to this one but
// with a different URL scheme and port
default ThreeArgsTest withScheme(String scheme) {
String urlWithScheme = urlWithScheme(scheme);
if (this instanceof OfHostFile) {
return new OfHostFile(character(), host(), file(), urlWithScheme);
}
throw new AssertionError("unexpected subclass: " + this.getClass());
}
@Override
default boolean early(int c) {
return (c < 31 || c == 127 || c == '/');
}
@Override
default boolean acceptDelayedException(Throwable exception) {
return URLArgTest.super.acceptDelayedException(exception)
|| "file".equalsIgnoreCase(scheme())
&& exception instanceof IOException;
}
record OfHostFile(int character, String host, String file, String url)
implements ThreeArgsTest {
}
static ThreeArgsTest ofHostFile(int c) {
String host = "local%shost".formatted(Character.toString(c));
String url = "http://" + host + "/";
return new OfHostFile(c, host, "/", url);
}
}
// Test data for the four args constructor
// public URL(String scheme, String host, int port, String file)
// throws MalformedURLException
sealed interface FourArgsTest extends URLArgTest {
// the host component
String host();
// the port component
int port();
// the path + query components
String file();
// Create a new test case identical to this one but
// with a different URL scheme and port
default FourArgsTest withScheme(String scheme) {
String urlWithScheme = urlWithScheme(scheme);
if (this instanceof OfHostFilePort) {
int port = URLArgTest.port(scheme);
return new OfHostFilePort(character(), host(), port, file(), urlWithScheme);
}
throw new AssertionError("unexpected subclass: " + this.getClass());
}
@Override
default boolean early(int c) {
return (c < 31 || c == 127 || c == '/');
}
@Override
default boolean acceptDelayedException(Throwable exception) {
return URLArgTest.super.acceptDelayedException(exception)
|| "file".equalsIgnoreCase(scheme())
&& exception instanceof IOException;
}
record OfHostFilePort(int character, String host, int port, String file, String url)
implements FourArgsTest {
}
static FourArgsTest ofHostPortFile(int c) {
String host = "local%shost".formatted(Character.toString(c));
String url = "http://" + host + "/";
int port = URLArgTest.port(URLArgTest.scheme(url));
return new OfHostFilePort(c, host, port, "/", url);
}
}
// Generate test data for the URL one arg constructor, with variations
// of the host component.
static Stream<OneArgTest> oneArgHostTests() {
List<OneArgTest> tests = new ArrayList<>();
List<OneArgTest> urls = new ArrayList<>();
urls.addAll((UNWISE + EXCLUDED_DELIMS).chars()
.mapToObj(OneArgTest::ofHost).toList());
urls.addAll(IntStream.concat(IntStream.range(0, 31), IntStream.of(127))
.mapToObj(OneArgTest::ofHost).toList());
for (String scheme : List.of("http", "https", "ftp")) {
for (var test : urls) {
tests.add(test.withScheme(scheme));
}
}
return tests.stream();
}
// Generate test data for the URL one arg constructor, with variations
// of the user info component.
static Stream<OneArgTest> oneArgUserInfoTests() {
List<OneArgTest> tests = new ArrayList<>();
List<OneArgTest> urls = new ArrayList<>();
urls.addAll(IntStream.concat(IntStream.range(0, 31), IntStream.of(127))
.mapToObj(OneArgTest::ofUserInfo).toList());
urls.add(OneArgTest.ofUserInfo('\\'));
for (String scheme : List.of("http", "https", "ftp")) {
for (var test : urls) {
tests.add(test.withScheme(scheme));
}
}
return tests.stream();
}
// Test data with all variations for the URL one arg
// constructor (spec)
static Stream<OneArgTest> oneArgTests() {
return Stream.concat(oneArgHostTests(), oneArgUserInfoTests());
}
// Test data with all variations for the URL two arg
// constructor (URL, spec)
static Stream<TwoArgsTest> twoArgTests() {
return oneArgTests().map(TwoArgsTest::ofOneArgTest);
}
// Generate test data for the URL three arguments constructor
// (scheme, host, file)
static Stream<ThreeArgsTest> threeArgsTests() {
List<ThreeArgsTest> urls = new ArrayList<>();
urls.addAll((UNWISE + EXCLUDED_DELIMS + DELIMS).chars()
.mapToObj(ThreeArgsTest::ofHostFile).toList());
urls.addAll(IntStream.concat(IntStream.range(0, 31), IntStream.of(127))
.mapToObj(ThreeArgsTest::ofHostFile).toList());
List<ThreeArgsTest> tests = new ArrayList<>();
for (String scheme : List.of("http", "https", "ftp", "file")) {
for (var test : urls) {
tests.add(test.withScheme(scheme));
}
}
return tests.stream();
}
// Generate test data for the URL four arguments constructor
// (scheme, host, port, file)
static Stream<FourArgsTest> fourArgsTests() {
List<FourArgsTest> urls = new ArrayList<>();
urls.addAll((UNWISE + EXCLUDED_DELIMS + DELIMS).chars()
.mapToObj(FourArgsTest::ofHostPortFile).toList());
urls.addAll(IntStream.concat(IntStream.range(0, 31), IntStream.of(127))
.mapToObj(FourArgsTest::ofHostPortFile).toList());
List<FourArgsTest> tests = new ArrayList<>();
for (String scheme : List.of("http", "https", "ftp", "file")) {
for (var test : urls) {
tests.add(test.withScheme(scheme));
}
}
return tests.stream();
}
@ParameterizedTest
@MethodSource("oneArgTests")
public void testOneArgConstructor(OneArgTest test) throws Exception {
int c = test.character();
String url = test.url();
if (EARLY_PARSING || test.early(c)) {
err.println("Early parsing: " + test.describe());
var exception = assertThrows(MalformedURLException.class, () -> {
new URL(url);
});
err.println("Got expected exception: " + exception);
} else {
err.println("Delayed parsing: " + test.describe());
URL u = new URL(url);
var exception = assertThrows(IOException.class, () -> {
u.openConnection().connect();
});
if (!test.acceptDelayedException(exception)) {
err.println("unexpected exception type: " + exception);
throw exception;
}
err.println("Got expected exception: " + exception);
assertFalse(exception instanceof ConnectException);
}
}
@ParameterizedTest
@MethodSource("twoArgTests")
public void testTwoArgConstructor(TwoArgsTest test) throws Exception {
int c = test.character();
String url = test.url();
String scheme = URLArgTest.scheme(url);
URL u = new URL(scheme, null,"");
if (EARLY_PARSING || test.early(c)) {
err.println("Early parsing: " + test.describe());
var exception = assertThrows(MalformedURLException.class, () -> {
new URL(u, url);
});
err.println("Got expected exception: " + exception);
} else {
err.println("Delayed parsing: " + test.describe());
URL u2 = new URL(u, url);
var exception = assertThrows(IOException.class, () -> {
u2.openConnection().connect();
});
if (!test.acceptDelayedException(exception)) {
err.println("unexpected exception type: " + exception);
throw exception;
}
err.println("Got expected exception: " + exception);
assertFalse(exception instanceof ConnectException);
}
}
@ParameterizedTest
@MethodSource("threeArgsTests")
public void testThreeArgsConstructor(ThreeArgsTest test) throws Exception {
int c = test.character();
String url = test.url();
if (EARLY_PARSING || test.early(c)) {
err.println("Early parsing: " + url);
var exception = assertThrows(MalformedURLException.class, () -> {
new URL(test.scheme(), test.host(), test.file());
});
err.println("Got expected exception: " + exception);
} else {
err.println("Delayed parsing: " + url);
URL u = new URL(test.scheme(), test.host(), test.file());
var exception = assertThrows(IOException.class, () -> {
u.openConnection().connect();
});
if (!test.acceptDelayedException(exception)) {
err.println("unexpected exception type: " + exception);
throw exception;
}
err.println("Got expected exception: " + exception);
assertFalse(exception instanceof ConnectException);
}
}
@ParameterizedTest
@MethodSource("fourArgsTests")
public void testFourArgsConstructor(FourArgsTest test) throws Exception {
int c = test.character();
String url = test.url();
if (EARLY_PARSING || test.early(c)) {
err.println("Early parsing: " + url);
var exception = assertThrows(MalformedURLException.class, () -> {
new URL(test.scheme(), test.host(), test.port(), test.file());
});
err.println("Got expected exception: " + exception);
} else {
err.println("Delayed parsing: " + url);
URL u = new URL(test.scheme(), test.host(), test.port(), test.file());
var exception = assertThrows(IOException.class, () -> {
u.openConnection().connect();
});
if (!test.acceptDelayedException(exception)) {
err.println("unexpected exception type: " + exception);
throw exception;
}
err.println("Got expected exception: " + exception);
assertFalse(exception instanceof ConnectException);
}
}
}