diff --git a/src/java.base/share/classes/jdk/internal/loader/URLClassPath.java b/src/java.base/share/classes/jdk/internal/loader/URLClassPath.java index 75418111f74..06e3442c244 100644 --- a/src/java.base/share/classes/jdk/internal/loader/URLClassPath.java +++ b/src/java.base/share/classes/jdk/internal/loader/URLClassPath.java @@ -903,7 +903,11 @@ public class URLClassPath { private FileLoader(URL url) throws IOException { super(url); 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(); @SuppressWarnings("deprecation") var _unused = normalizedBase = new URL(getBaseURL(), "."); diff --git a/src/java.base/share/classes/sun/net/www/ParseUtil.java b/src/java.base/share/classes/sun/net/www/ParseUtil.java index def688ad96a..3a35f86ab88 100644 --- a/src/java.base/share/classes/sun/net/www/ParseUtil.java +++ b/src/java.base/share/classes/sun/net/www/ParseUtil.java @@ -171,6 +171,7 @@ public final class ParseUtil { * Returns a new String constructed from the specified String by replacing * the URL escape sequences and UTF8 encoding with the characters they * represent. + * @throws IllegalArgumentException if {@code s} could not be decoded */ public static String decode(String s) { int n = s.length(); diff --git a/src/java.base/unix/classes/jdk/internal/loader/FileURLMapper.java b/src/java.base/unix/classes/jdk/internal/loader/FileURLMapper.java index 6507b2961b9..79d0c6faf5b 100644 --- a/src/java.base/unix/classes/jdk/internal/loader/FileURLMapper.java +++ b/src/java.base/unix/classes/jdk/internal/loader/FileURLMapper.java @@ -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. * * This code is free software; you can redistribute it and/or modify it @@ -25,8 +25,10 @@ package jdk.internal.loader; +import java.io.IOException; import java.net.URL; import java.io.File; + import sun.net.www.ParseUtil; /** @@ -40,12 +42,12 @@ import sun.net.www.ParseUtil; * @author Michael McMahon */ -public class FileURLMapper { +final class FileURLMapper { - URL url; - String path; + private final URL url; + private String path; - public FileURLMapper (URL url) { + FileURLMapper(URL url) { this.url = url; } @@ -53,15 +55,18 @@ public class FileURLMapper { * @return the platform specific path corresponding to the URL * so long as the URL does not contain a hostname in the authority field. */ - - public String getPath () { + String getPath() throws IOException { if (path != null) { return path; } String host = url.getHost(); if (host == null || host.isEmpty() || "localhost".equalsIgnoreCase(host)) { path = url.getFile(); - path = ParseUtil.decode(path); + try { + path = ParseUtil.decode(path); + } catch (IllegalArgumentException iae) { + throw new IOException(iae); + } } return path; } @@ -69,12 +74,12 @@ public class FileURLMapper { /** * Checks whether the file identified by the URL exists. */ - public boolean exists () { - String s = getPath (); + boolean exists() throws IOException { + String s = getPath(); if (s == null) { return false; } else { - File f = new File (s); + File f = new File(s); return f.exists(); } } diff --git a/src/java.base/windows/classes/jdk/internal/loader/FileURLMapper.java b/src/java.base/windows/classes/jdk/internal/loader/FileURLMapper.java index 0053cf70e9d..b72bc31b206 100644 --- a/src/java.base/windows/classes/jdk/internal/loader/FileURLMapper.java +++ b/src/java.base/windows/classes/jdk/internal/loader/FileURLMapper.java @@ -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. * * This code is free software; you can redistribute it and/or modify it @@ -25,8 +25,10 @@ package jdk.internal.loader; +import java.io.IOException; import java.net.URL; import java.io.File; + import sun.net.www.ParseUtil; /** @@ -36,12 +38,12 @@ import sun.net.www.ParseUtil; * @author Michael McMahon */ -public class FileURLMapper { +final class FileURLMapper { - URL url; - String file; + private final URL url; + private String file; - public FileURLMapper (URL url) { + FileURLMapper (URL url) { this.url = url; } @@ -49,8 +51,7 @@ public class FileURLMapper { * @return the platform specific path corresponding to the URL, and in particular * returns a UNC when the authority contains a hostname */ - - public String getPath () { + String getPath() throws IOException { if (file != null) { return file; } @@ -63,13 +64,17 @@ public class FileURLMapper { return file; } String path = url.getFile().replace('/', '\\'); - file = ParseUtil.decode(path); + try { + file = ParseUtil.decode(path); + } catch (IllegalArgumentException iae) { + throw new IOException(iae); + } return file; } - public boolean exists() { + boolean exists() throws IOException { String path = getPath(); - File f = new File (path); + File f = new File(path); return f.exists(); } } diff --git a/test/jdk/jdk/internal/loader/URLClassPath/ClassPathUnusableURLs.java b/test/jdk/jdk/internal/loader/URLClassPath/ClassPathUnusableURLs.java new file mode 100644 index 00000000000..420425f37fc --- /dev/null +++ b/test/jdk/jdk/internal/loader/URLClassPath/ClassPathUnusableURLs.java @@ -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 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 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() + }; + } +}