--- old/src/java.base/share/classes/java/lang/ClassLoader.java 2018-09-10 14:12:17.443490752 -0400 +++ new/src/java.base/share/classes/java/lang/ClassLoader.java 2018-09-10 14:12:16.967490752 -0400 @@ -68,6 +68,7 @@ import jdk.internal.ref.CleanerFactory; import jdk.internal.reflect.CallerSensitive; import jdk.internal.reflect.Reflection; +import jdk.internal.util.PathParser; import sun.reflect.misc.ReflectUtil; import sun.security.util.SecurityConstants; @@ -2555,53 +2556,8 @@ private static String[] initializePath(String propName) { String ldPath = System.getProperty(propName, ""); - int ldLen = ldPath.length(); - char ps = File.pathSeparatorChar; - int psCount = 0; - - if (ClassLoaderHelper.allowsQuotedPathElements && - ldPath.indexOf('\"') >= 0) { - // First, remove quotes put around quoted parts of paths. - // Second, use a quotation mark as a new path separator. - // This will preserve any quoted old path separators. - char[] buf = new char[ldLen]; - int bufLen = 0; - for (int i = 0; i < ldLen; ++i) { - char ch = ldPath.charAt(i); - if (ch == '\"') { - while (++i < ldLen && - (ch = ldPath.charAt(i)) != '\"') { - buf[bufLen++] = ch; - } - } else { - if (ch == ps) { - psCount++; - ch = '\"'; - } - buf[bufLen++] = ch; - } - } - ldPath = new String(buf, 0, bufLen); - ldLen = bufLen; - ps = '\"'; - } else { - for (int i = ldPath.indexOf(ps); i >= 0; - i = ldPath.indexOf(ps, i + 1)) { - psCount++; - } - } - String[] paths = new String[psCount + 1]; - int pathStart = 0; - for (int j = 0; j < psCount; ++j) { - int pathEnd = ldPath.indexOf(ps, pathStart); - paths[j] = (pathStart < pathEnd) ? - ldPath.substring(pathStart, pathEnd) : "."; - pathStart = pathEnd + 1; - } - paths[psCount] = (pathStart < ldLen) ? - ldPath.substring(pathStart, ldLen) : "."; - return paths; + return PathParser.parsePath(ldPath, "."); } // Invoked in the java.lang.Runtime class to implement load and loadLibrary. --- old/src/java.base/share/classes/java/nio/file/Paths.java 2018-09-10 14:12:18.839490752 -0400 +++ new/src/java.base/share/classes/java/nio/file/Paths.java 2018-09-10 14:12:18.375490752 -0400 @@ -25,13 +25,22 @@ package java.nio.file; -import java.nio.file.spi.FileSystemProvider; +import jdk.internal.util.PathParser; + +import java.io.File; import java.net.URI; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; /** * This class consists exclusively of static methods that return a {@link Path} * by converting a path string or {@link URI}. * + *

Unless otherwise noted, passing a {@code null} argument to a method + * in this class will cause a {@link NullPointerException} to be thrown. + * * @apiNote * It is recommended to obtain a {@code Path} via the {@code Path.of} methods * instead of via the {@code get} methods defined in this class as this class @@ -96,4 +105,55 @@ public static Path get(URI uri) { return Path.of(uri); } + + /** + * Returns a list of path strings parsed from a string with empty paths removed. + * The {@link File#pathSeparator} is used to split the string and + * empty strings are removed. A list is created of the remaining strings. + *

+ * The {@code pathSeparator} character can be included in a path + * on operating systems that support quoting segments of the string. + * + * @implNote On Windows, quoting of path segments is supported. + * Each {@link File#pathSeparator} between pairs of quotation marks (@code 'U+0022') + * is considered an ordinary character and the quotes are omitted from the path. + * An unmatched double-quote is matched by the end of the string. + * + * @param path a {@code non-null} string containing paths separated by + * {@link File#pathSeparator}. + * @return a {@code non-null} immutable list of strings for each non-empty path + */ + public static List pathToStrings(String path) { + Objects.requireNonNull(path, "path"); + return List.of(PathParser.parsePath(path, null)); + } + + /** + * Returns a list of Paths parsed from a string with empty paths removed. + * The {@link File#pathSeparator} is used to split the string and + * empty strings are removed. A list is created of the remaining strings after + * mapping each string using {@link Path#of Path.of} using the + * {@link FileSystems#getDefault default} {@link FileSystem}. + *

+ * The {@code pathSeparator} character can be included in a path + * on operating systems that support quoting segments of the string. + * + * @implNote On Windows, quoting of path segments is supported. + * Each {@link File#pathSeparator} between pairs of quotation marks (@code 'U+0022') + * is considered an ordinary character and the quotes are omitted from the path. + * An unmatched double-quote is matched by the end of the string. + * + * @param path a {@code non-null} string containing paths separated by + * {@link File#pathSeparator}. + * @return a {@code non-null} immutable list of Paths for each non-empty path + * + * @throws InvalidPathException + * if each path string cannot be converted to a {@code Path} + */ + public static List pathToPaths(String path) { + Objects.requireNonNull(path, "path"); + return Arrays.stream(PathParser.parsePath(path, null)) + .map(s -> Path.of(s)) + .collect(Collectors.toList()); + } } --- old/src/java.base/share/classes/jdk/internal/loader/URLClassPath.java 2018-09-10 14:12:20.543490752 -0400 +++ new/src/java.base/share/classes/jdk/internal/loader/URLClassPath.java 2018-09-10 14:12:19.951490752 -0400 @@ -48,7 +48,6 @@ import java.security.cert.Certificate; import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; @@ -56,6 +55,7 @@ import java.util.LinkedList; import java.util.List; import java.util.NoSuchElementException; +import java.util.Objects; import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; @@ -70,6 +70,7 @@ import jdk.internal.misc.JavaNetURLAccess; import jdk.internal.misc.JavaUtilZipFileAccess; import jdk.internal.misc.SharedSecrets; +import jdk.internal.util.PathParser; import jdk.internal.util.jar.InvalidJarIndexError; import jdk.internal.util.jar.JarIndex; import sun.net.util.URLUtil; @@ -101,7 +102,7 @@ } /* The original search path of URLs. */ - private final ArrayList path; + private final List path; /* The deque of unopened URLs */ private final ArrayDeque unopenedUrls; @@ -164,6 +165,8 @@ /** * Constructs a URLClassPath from a class path string. + * Empty path strings are later used to open a directory that defaults + * to the current directory. * * @param cp the class path string * @param skipEmptyElements indicates if empty elements are ignored or @@ -172,29 +175,21 @@ * @apiNote Used to create the application class path. */ URLClassPath(String cp, boolean skipEmptyElements) { - ArrayList path = new ArrayList<>(); - if (cp != null) { - // map each element of class path to a file URL - int off = 0, next; - do { - next = cp.indexOf(File.pathSeparator, off); - String element = (next == -1) - ? cp.substring(off) - : cp.substring(off, next); - if (element.length() > 0 || !skipEmptyElements) { - URL url = toFileURL(element); - if (url != null) path.add(url); - } - off = next + 1; - } while (next != -1); - } + String[] strings = PathParser + .parsePath(Objects.requireNonNullElse(cp, ""), + skipEmptyElements ? null : ""); + int size = strings.length; + ArrayList path = new ArrayList<>(size); + ArrayDeque unopenedUrls = new ArrayDeque<>(size); // can't use ArrayDeque#addAll or new ArrayDeque(Collection); // it's too early in the bootstrap to trigger use of lambdas - int size = path.size(); - ArrayDeque unopenedUrls = new ArrayDeque<>(size); - for (int i = 0; i < size; i++) - unopenedUrls.add(path.get(i)); + // Add a URL for each path element to the paths and the unopenedUrls. + for (String s : strings) { + URL url = toFileURL(s); + path.add(url); + unopenedUrls.add(url); + } this.unopenedUrls = unopenedUrls; this.path = path; --- old/src/java.base/share/classes/jdk/internal/module/ModuleBootstrap.java 2018-09-10 14:12:22.127490752 -0400 +++ new/src/java.base/share/classes/jdk/internal/module/ModuleBootstrap.java 2018-09-10 14:12:21.491490752 -0400 @@ -34,6 +34,7 @@ import java.lang.module.ResolvedModule; import java.net.URI; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -54,7 +55,6 @@ import jdk.internal.misc.JavaLangAccess; import jdk.internal.misc.JavaLangModuleAccess; import jdk.internal.misc.SharedSecrets; -import jdk.internal.misc.VM; import jdk.internal.perf.PerfCounter; /** @@ -543,12 +543,9 @@ if (s == null) { return null; } else { - String[] dirs = s.split(File.pathSeparator); - Path[] paths = new Path[dirs.length]; - int i = 0; - for (String dir: dirs) { - paths[i++] = Path.of(dir); - } + Path[] paths = Paths.pathToStrings(s).stream() + .map(Path::of) + .toArray(Path[]::new); return ModulePath.of(patcher, paths); } } --- old/src/java.base/share/classes/jdk/internal/module/ModulePathValidator.java 2018-09-10 14:12:23.383490752 -0400 +++ new/src/java.base/share/classes/jdk/internal/module/ModulePathValidator.java 2018-09-10 14:12:22.903490752 -0400 @@ -25,7 +25,6 @@ package jdk.internal.module; -import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.lang.module.FindException; @@ -37,12 +36,12 @@ import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; +import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.Optional; -import java.util.stream.Stream; /** * A validator to check for errors and conflicts between modules. @@ -78,7 +77,8 @@ // upgrade module path String value = System.getProperty("jdk.module.upgrade.path"); if (value != null) { - Stream.of(value.split(File.pathSeparator)) + Paths.pathToStrings(value) + .stream() .map(Path::of) .forEach(validator::scan); } @@ -91,7 +91,8 @@ // application module path value = System.getProperty("jdk.module.path"); if (value != null) { - Stream.of(value.split(File.pathSeparator)) + Paths.pathToStrings(value) + .stream() .map(Path::of) .forEach(validator::scan); } --- old/src/java.base/share/classes/sun/security/tools/PathList.java 2018-09-10 14:12:25.111490752 -0400 +++ new/src/java.base/share/classes/sun/security/tools/PathList.java 2018-09-10 14:12:24.651490752 -0400 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004, 2011, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2004, 2018, 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 @@ -27,11 +27,9 @@ import java.io.File; import java.io.IOException; -import java.lang.String; -import java.util.StringTokenizer; -import java.net.URL; -import java.net.URLClassLoader; import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Paths; /** * A utility class for handle path list @@ -63,21 +61,10 @@ * @return the resulting array of directory and JAR file URLs */ public static URL[] pathToURLs(String path) { - StringTokenizer st = new StringTokenizer(path, File.pathSeparator); - URL[] urls = new URL[st.countTokens()]; - int count = 0; - while (st.hasMoreTokens()) { - URL url = fileToURL(new File(st.nextToken())); - if (url != null) { - urls[count++] = url; - } - } - if (urls.length != count) { - URL[] tmp = new URL[count]; - System.arraycopy(urls, 0, tmp, 0, count); - urls = tmp; - } - return urls; + return Paths.pathToStrings(path) + .stream() + .map(s -> fileToURL(new File(s))) + .toArray(URL[]::new); } /** --- /dev/null 2018-09-07 16:39:11.960000000 -0400 +++ new/src/java.base/share/classes/jdk/internal/util/PathParser.java 2018-09-10 14:12:32.071490752 -0400 @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2007, 2018, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.util; + +import sun.security.action.GetPropertyAction; + +import java.io.File; + +/** + * Parse path strings and return a List of paths and path strings. + * Implementation relocated and improved from ClassLoader. + */ +public class PathParser { + + private static final boolean allowsQuotedPathElements = initQuotesAllowed(); + + private static boolean initQuotesAllowed() { + return GetPropertyAction.privilegedGetProperty("os.name").contains("Windows"); + } + + /** + * Returns an array of path strings parsed from a string. + * Empty paths, identified by leading, trailing, and adjacent separator characters, + * can be omitted or replaced with a non-null provided path. + *

+ * The string is split using {@link File#pathSeparator}. + * The {@code pathSeparator} character can be included in a path + * on operating systems that support quoting segments of the string. + * + * @param path a string containing paths separated by the {@link File#pathSeparator} + * @param emptyPath a path to replace an empty path or {@code null} to omit empty paths + * @return an non-null array of strings for each path + * @implNote On Windows, quoting of path segments is supported. Each {@link File#pathSeparator} + * between pairs of quotation marks (@code 'U+0022') is considered an ordinary character + * and the quotes are omitted from the path. + * An unmatched quote is matched by the end of the string. + * + * @see java.nio.file.Paths#pathToStrings(String) + * @see java.nio.file.Paths#pathToPaths(String) + */ + public static String[] parsePath(String path, String emptyPath) { + char ps = File.pathSeparatorChar; + int psCount = 0; + int emptyCount = 0; + if (allowsQuotedPathElements && + path.indexOf('\"') >= 0) { + // First, remove quotes put around quoted parts of paths. + // Second, use a quotation mark as a new path separator. + // This will preserve any quoted old path separators. + int ldLen = path.length(); + char[] buf = new char[ldLen]; + int bufLen = 0; + int i, j; + for (i = 0, j = 0; i < ldLen; ++i) { + char ch = path.charAt(i); + if (ch == '\"') { + while (++i < ldLen && + (ch = path.charAt(i)) != '\"') { + buf[bufLen++] = ch; + } + } else { + if (ch == ps) { + psCount++; + emptyCount += (j < bufLen) ? 0 : 1; // count empty elements + ch = '\"'; + j = bufLen + 1; // start of next element + } + buf[bufLen++] = ch; + } + } + emptyCount += (j < bufLen) ? 0 : 1; + path = new String(buf, 0, bufLen); + ldLen = bufLen; + ps = '\"'; + } else { + int i, j; + for (i = path.indexOf(ps), j = 0; i >= 0; + j = i + 1, i = path.indexOf(ps, j)) { + psCount++; + emptyCount += (j < i) ? 0 : 1; // count empty elements + } + emptyCount += (j < path.length()) ? 0 : 1; + } + + if (emptyPath == null) { + // Do not save space to replace empty elements + psCount -= emptyCount; + } + + String[] paths = new String[psCount + 1]; + int next = 0; + int startPath = 0; + int endPath; + for (endPath = path.indexOf(ps); endPath >= 0; + startPath = endPath + 1, endPath = path.indexOf(ps, startPath)) { + if (endPath > startPath) { + paths[next++] = path.substring(startPath, endPath); + } else if (emptyPath != null) { + paths[next++] = emptyPath; + } + } + if (path.length() > startPath) { + paths[next] = path.substring(startPath); + } else if (emptyPath != null) { + paths[next] = emptyPath; + } + return paths; + } +} --- /dev/null 2018-09-07 16:39:11.960000000 -0400 +++ new/test/jdk/java/nio/file/Path/ParsePathTest.java 2018-09-10 14:12:44.911490752 -0400 @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2018, 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.lang.System; +import java.io.File; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Collectors; + +import java.nio.file.Paths; + +import jdk.internal.util.PathParser; +import org.testng.Assert; +import org.testng.IMethodInstance; +import org.testng.IMethodInterceptor; +import org.testng.TestListenerAdapter; +import org.testng.TestNG; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +/* + * @test + * @bug 4463345 4244670 8030781 + * @summary Tests of parsing paths via public and internal APIs + * @modules java.base/jdk.internal.util:+open + * @run testng/othervm ParsePathTest + */ + +@Test +public class ParsePathTest { + + static final String SP = File.pathSeparator; + + @DataProvider(name = "pathData") + static Object[][] pathData() { + if (System.getProperty("os.name").contains("Windows")) { + return new Object[][]{ + // Common path lists + {"", List.of(".")}, + {SP, List.of(".", ".")}, + {"a" + SP, List.of("a", ".")}, + {SP + "b", List.of(".", "b")}, + {"a" + SP + SP + "b", List.of("a", ".", "b")}, + + // on Windows parts of paths may be quoted + {"\"\"", List.of(".")}, + {"\"\"" + SP, List.of(".", ".")}, + {SP + "\"\"", List.of(".", ".")}, + {"a" + SP + "\"b\"" + SP, List.of("a", "b", ".")}, + {SP + "\"a\"" + SP + SP + "b", List.of(".", "a", ".", "b")}, + {"\"a\"" + SP + "\"b\"", List.of("a", "b")}, + {"\"/a/\"b" + SP + "c", List.of("/a/b", "c")}, + {"\"/a;b\"" + SP + "c", List.of("/a;b", "c")}, + {"\"/a" + SP + "b\"" + SP + "c", List.of("/a" + SP + "b", "c")}, + {"/\"a\"\";\"\"b\"" + SP + "\"c\"", List.of("/a;b", "c")}, + + // Unmatched trailing quotes + {"\"c\"" + SP + "/\"a\"\";\"\"b", List.of("c", "/a;b")}, + {"\"c\"" + SP + "/\"a\"\";b", List.of("c", "/a;b")}, + + }; + } else { + return new Object[][]{ + {"", List.of(".")}, + {SP, List.of(".", ".")}, + {"a" + SP, List.of("a", ".")}, + {SP + "b", List.of(".", "b")}, + {"a" + SP + SP + "b", List.of("a", ".", "b")}, + }; + } + } + + /** + * Test public API that produces lists of Path. + */ + @Test(dataProvider = "pathData") + static void checkPathToPaths(String path, Listexpected) { + List newExpected = expected.stream() + .filter(s -> !s.equals(".")) + .map( s -> Path.of(s)) + .collect(Collectors.toList()); + List s = Paths.pathToPaths(path); + Assert.assertEquals(s, newExpected, path + " as " + newExpected.toString()); + } + + /** + * Test public API that produces lists of String. + */ + @Test(dataProvider = "pathData") + static void checkpathToStrings(String path, Listexpected) { + List newExpected = expected.stream() + .filter(s -> !s.equals(".")) + .collect(Collectors.toList()); + List s = Paths.pathToStrings(path); + Assert.assertEquals(s, newExpected, path + " as " + newExpected.toString()); + } + + /** + * Test internal API that parses to arrays of String with replaced empty path. + * @param path a path + * @param expected a list of the expected path strings + */ + @Test(dataProvider = "pathData") + static void checkpathToStringsRepl(String path, List expected) { + List s = List.of(PathParser.parsePath(path, ".")); + Assert.assertEquals(s, expected, path + " as " + expected.toString()); + } + + /** + * Test internal API that parses to arrays of String omitting empty paths. + * @param path a path + * @param expected a list of the expected path strings + */ + @Test(dataProvider = "pathData") + static void checkpathToStringsNoRepl(String path, Listexpected) { + List newExpected = expected.stream() + .filter(s -> !s.equals(".")) + .collect(Collectors.toList()); + List s = List.of(PathParser.parsePath(path, null)); + Assert.assertEquals(s, newExpected, path + " as " + newExpected.toString()); + } + + + /** + * Main to allow stand alone execution. + * @param args command line args - none expected + */ + @SuppressWarnings("raw_types") + @Test(enabled=false) + public static void main(String[] args) { + TestListenerAdapter tla = new TestListenerAdapter(); + + Class[] testclass = {ParsePathTest.class}; + TestNG testng = new TestNG(); + testng.setTestClasses(testclass); + testng.addListener(tla); + if (args.length > 0) { + IMethodInterceptor intercept = (m, c) -> { + List x = m.stream() + .filter(m1 -> m1.getMethod().getMethodName().contains(args[0])) + .collect(Collectors.toList()); + return x; + }; + testng.setMethodInterceptor(intercept); + } + testng.run(); + tla.getPassedTests() + .stream().forEach(t -> System.out.printf("Passed: %s%s%n", t.getName(), + List.of(t.getParameters()))); + tla.getFailedTests() + .stream().forEach(t -> System.out.printf("Failed: %s%s%n", t.getName(), + List.of(t.getParameters()))); + } +}