8344908: URLClassPath should not propagate IllegalArgumentException when finding resources in classpath URLs

Reviewed-by: alanb
This commit is contained in:
Jaikiran Pai 2024-11-28 07:54:00 +00:00
parent ce9d543eb1
commit 81c44e5eb4
5 changed files with 225 additions and 22 deletions

View File

@ -903,7 +903,11 @@ public class URLClassPath {
private FileLoader(URL url) throws IOException { private FileLoader(URL url) throws IOException {
super(url); super(url);
String path = url.getFile().replace('/', File.separatorChar); String path = url.getFile().replace('/', File.separatorChar);
path = ParseUtil.decode(path); try {
path = ParseUtil.decode(path);
} catch (IllegalArgumentException iae) {
throw new IOException(iae);
}
dir = (new File(path)).getCanonicalFile(); dir = (new File(path)).getCanonicalFile();
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
var _unused = normalizedBase = new URL(getBaseURL(), "."); var _unused = normalizedBase = new URL(getBaseURL(), ".");

View File

@ -171,6 +171,7 @@ public final class ParseUtil {
* Returns a new String constructed from the specified String by replacing * Returns a new String constructed from the specified String by replacing
* the URL escape sequences and UTF8 encoding with the characters they * the URL escape sequences and UTF8 encoding with the characters they
* represent. * represent.
* @throws IllegalArgumentException if {@code s} could not be decoded
*/ */
public static String decode(String s) { public static String decode(String s) {
int n = s.length(); int n = s.length();

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2002, 2003, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2002, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -25,8 +25,10 @@
package jdk.internal.loader; package jdk.internal.loader;
import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.io.File; import java.io.File;
import sun.net.www.ParseUtil; import sun.net.www.ParseUtil;
/** /**
@ -40,12 +42,12 @@ import sun.net.www.ParseUtil;
* @author Michael McMahon * @author Michael McMahon
*/ */
public class FileURLMapper { final class FileURLMapper {
URL url; private final URL url;
String path; private String path;
public FileURLMapper (URL url) { FileURLMapper(URL url) {
this.url = url; this.url = url;
} }
@ -53,15 +55,18 @@ public class FileURLMapper {
* @return the platform specific path corresponding to the URL * @return the platform specific path corresponding to the URL
* so long as the URL does not contain a hostname in the authority field. * so long as the URL does not contain a hostname in the authority field.
*/ */
String getPath() throws IOException {
public String getPath () {
if (path != null) { if (path != null) {
return path; return path;
} }
String host = url.getHost(); String host = url.getHost();
if (host == null || host.isEmpty() || "localhost".equalsIgnoreCase(host)) { if (host == null || host.isEmpty() || "localhost".equalsIgnoreCase(host)) {
path = url.getFile(); path = url.getFile();
path = ParseUtil.decode(path); try {
path = ParseUtil.decode(path);
} catch (IllegalArgumentException iae) {
throw new IOException(iae);
}
} }
return path; return path;
} }
@ -69,12 +74,12 @@ public class FileURLMapper {
/** /**
* Checks whether the file identified by the URL exists. * Checks whether the file identified by the URL exists.
*/ */
public boolean exists () { boolean exists() throws IOException {
String s = getPath (); String s = getPath();
if (s == null) { if (s == null) {
return false; return false;
} else { } else {
File f = new File (s); File f = new File(s);
return f.exists(); return f.exists();
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2002, 2003, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2002, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -25,8 +25,10 @@
package jdk.internal.loader; package jdk.internal.loader;
import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.io.File; import java.io.File;
import sun.net.www.ParseUtil; import sun.net.www.ParseUtil;
/** /**
@ -36,12 +38,12 @@ import sun.net.www.ParseUtil;
* @author Michael McMahon * @author Michael McMahon
*/ */
public class FileURLMapper { final class FileURLMapper {
URL url; private final URL url;
String file; private String file;
public FileURLMapper (URL url) { FileURLMapper (URL url) {
this.url = url; this.url = url;
} }
@ -49,8 +51,7 @@ public class FileURLMapper {
* @return the platform specific path corresponding to the URL, and in particular * @return the platform specific path corresponding to the URL, and in particular
* returns a UNC when the authority contains a hostname * returns a UNC when the authority contains a hostname
*/ */
String getPath() throws IOException {
public String getPath () {
if (file != null) { if (file != null) {
return file; return file;
} }
@ -63,13 +64,17 @@ public class FileURLMapper {
return file; return file;
} }
String path = url.getFile().replace('/', '\\'); String path = url.getFile().replace('/', '\\');
file = ParseUtil.decode(path); try {
file = ParseUtil.decode(path);
} catch (IllegalArgumentException iae) {
throw new IOException(iae);
}
return file; return file;
} }
public boolean exists() { boolean exists() throws IOException {
String path = getPath(); String path = getPath();
File f = new File (path); File f = new File(path);
return f.exists(); return f.exists();
} }
} }

View File

@ -0,0 +1,188 @@
/*
* Copyright (c) 2024, 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.
*/
import java.io.OutputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import jdk.internal.loader.Resource;
import jdk.internal.loader.URLClassPath;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assumptions.abort;
/*
* @test
* @bug 8344908
* @summary verify that when locating resources, the URLClassPath can function properly
* without throwing unexpected exceptions when any URL in the classpath is unusable
* @modules java.base/jdk.internal.loader
* @run junit ClassPathUnusableURLs
*/
public class ClassPathUnusableURLs {
private static final Path SCRATCH_DIR = Path.of(".").normalize();
private static final String RESOURCE_NAME = "foo.txt";
private static final String SMILEY_EMOJI = "\uD83D\uDE00";
private static Path ASCII_DIR;
private static Path EMOJI_DIR;
private static Path JAR_FILE_IN_EMOJI_DIR;
private static int NUM_EXPECTED_LOCATED_RESOURCES;
@BeforeAll
static void beforeAll() throws Exception {
try {
EMOJI_DIR = Files.createTempDirectory(SCRATCH_DIR, SMILEY_EMOJI);
} catch (IllegalArgumentException iae) {
iae.printStackTrace(); // for debug purpose
// if we can't create a directory with an emoji in its path name,
// then skip the entire test
abort("Skipping test since emoji directory couldn't be created: " + iae);
}
// successful creation of the dir, continue with the test
Files.createFile(EMOJI_DIR.resolve(RESOURCE_NAME));
ASCII_DIR = Files.createTempDirectory(SCRATCH_DIR, "test-urlclasspath");
Files.createFile(ASCII_DIR.resolve(RESOURCE_NAME));
// create a jar file containing the resource
JAR_FILE_IN_EMOJI_DIR = Files.createTempDirectory(SCRATCH_DIR, SMILEY_EMOJI)
.resolve("foo.jar");
final Manifest manifest = new Manifest();
manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
try (OutputStream fos = Files.newOutputStream(JAR_FILE_IN_EMOJI_DIR);
JarOutputStream jos = new JarOutputStream(fos, manifest)) {
final JarEntry jarEntry = new JarEntry(RESOURCE_NAME);
jos.putNextEntry(jarEntry);
jos.write("hello".getBytes(US_ASCII));
jos.closeEntry();
}
// Even if the resource is present in more than one classpath element,
// we expect it to be found by the URLClassPath only in the path which has just ascii
// characters. URLClassPath currently doesn't have the ability to serve resources
// from paths containing emoji character(s).
NUM_EXPECTED_LOCATED_RESOURCES = 1;
}
/**
* Constructs a URLClassPath and then exercises the URLClassPath.findResource()
* and URLClassPath.findResources() methods and expects them to return the expected
* resources.
*/
@Test
void testFindResource() {
// start an empty URL classpath
final URLClassPath urlc = new URLClassPath(new URL[0]);
final String[] classpathElements = getClassPathElements();
try {
// use addFile() to construct classpath
for (final String path : classpathElements) {
urlc.addFile(path);
}
// findResource()
assertNotNull(urlc.findResource(RESOURCE_NAME), "findResource() failed to locate"
+ " resource: " + RESOURCE_NAME + " in classpath: "
+ Arrays.toString(classpathElements));
// findResources()
final Enumeration<URL> locatedResources = urlc.findResources(RESOURCE_NAME);
assertNotNull(locatedResources, "findResources() failed to"
+ " locate resource: " + RESOURCE_NAME + " in classpath: "
+ Arrays.toString(classpathElements));
int numFound = 0;
while (locatedResources.hasMoreElements()) {
System.out.println("located " + locatedResources.nextElement()
+ " for resource " + RESOURCE_NAME);
numFound++;
}
assertEquals(NUM_EXPECTED_LOCATED_RESOURCES, numFound,
"unexpected number of resources located for " + RESOURCE_NAME);
} finally {
urlc.closeLoaders();
}
}
/**
* Constructs a URLClassPath and then exercises the URLClassPath.getResource()
* and URLClassPath.getResources() methods and expects them to return the expected
* resources.
*/
@Test
void testGetResource() {
// start an empty URL classpath
final URLClassPath urlc = new URLClassPath(new URL[0]);
final String[] classpathElements = getClassPathElements();
try {
// use addFile() to construct classpath
for (final String path : classpathElements) {
urlc.addFile(path);
}
// getResource()
assertNotNull(urlc.getResource(RESOURCE_NAME), "getResource() failed to locate"
+ " resource: " + RESOURCE_NAME + " in classpath: "
+ Arrays.toString(classpathElements));
// getResources()
final Enumeration<Resource> locatedResources = urlc.getResources(RESOURCE_NAME);
assertNotNull(locatedResources, "getResources() failed to"
+ " locate resource: " + RESOURCE_NAME + " in classpath: "
+ Arrays.toString(classpathElements));
int numFound = 0;
while (locatedResources.hasMoreElements()) {
System.out.println("located " + locatedResources.nextElement().getURL()
+ " for resource " + RESOURCE_NAME);
numFound++;
}
assertEquals(NUM_EXPECTED_LOCATED_RESOURCES, numFound,
"unexpected number of resources located for " + RESOURCE_NAME);
} finally {
urlc.closeLoaders();
}
}
private static String[] getClassPathElements() {
// Maintain the order - in context of this test, paths with emojis
// or those which can't serve the resource should come before the
// path that can serve the resource.
return new String[]{
// non-existent path
ASCII_DIR.resolve("non-existent").toString(),
// existing emoji dir
EMOJI_DIR.toString(),
// existing jar file in a emoji dir
JAR_FILE_IN_EMOJI_DIR.toString(),
// existing ascii dir
ASCII_DIR.toString()
};
}
}