From 02a63ff1b75c055aa2861f5acd9c63b344aad124 Mon Sep 17 00:00:00 2001 From: "David M. Lloyd" Date: Mon, 25 Mar 2024 09:46:46 -0500 Subject: [PATCH] Support multiple nesting of JAR files * Make sure that `jar:file:///...` are properly formed (the sub-URL must have a scheme of `file`) * When there is no `!` in the JAR URL, treat the file as a JAR and process its root path * Otherwise recursively resolve each segment which is `!`-terminated --- .../common/classloader/ClassPathUtils.java | 83 +++++++++++++------ 1 file changed, 59 insertions(+), 24 deletions(-) diff --git a/classloader/src/main/java/io/smallrye/common/classloader/ClassPathUtils.java b/classloader/src/main/java/io/smallrye/common/classloader/ClassPathUtils.java index e089d2ca..a5e441ac 100644 --- a/classloader/src/main/java/io/smallrye/common/classloader/ClassPathUtils.java +++ b/classloader/src/main/java/io/smallrye/common/classloader/ClassPathUtils.java @@ -8,12 +8,12 @@ import java.net.URL; import java.net.URLConnection; import java.nio.file.FileSystem; -import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.spi.FileSystemProvider; import java.util.Enumeration; +import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; @@ -105,6 +105,19 @@ public static void consumeAsPath(URL url, Consumer consumer) { }); } + private static final FileSystemProvider JAR_PROVIDER; + + static { + final ClassLoader ccl = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader()); + JAR_PROVIDER = FileSystemProvider.installedProviders().stream().filter(p -> p.getScheme().equals("jar")).findFirst() + .orElseThrow(); + } finally { + Thread.currentThread().setContextClassLoader(ccl); + } + } + /** * Attempts to represent a resource as a local file system path to be processed by a function. * If a resource appears to be an actual file or a directory, it is simply passed to the function as-is. @@ -118,33 +131,26 @@ public static void consumeAsPath(URL url, Consumer consumer) { */ public static R processAsPath(URL url, Function function) { if (JAR.equals(url.getProtocol())) { - final ClassLoader ccl = Thread.currentThread().getContextClassLoader(); - try { - // We are loading "installed" FS providers that are loaded from the system classloader anyway - // To avoid potential ClassCastExceptions we are setting the context classloader to the system one - Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader()); - FileSystemProvider.installedProviders(); - } finally { - Thread.currentThread().setContextClassLoader(ccl); - } - final String file = url.getFile(); - final int exclam = file.lastIndexOf('!'); - final Path jar; + final int exclam = file.indexOf('!'); try { - jar = toLocalPath(exclam >= 0 ? new URL(file.substring(0, exclam)) : url); + URL fileUrl; + String subPath; + if (exclam == -1) { + // assume the first element is a JAR file, not a plain file, since it was a `jar:` URL + fileUrl = new URL(file); + subPath = "/"; + } else { + fileUrl = new URL(file.substring(0, exclam)); + subPath = file.substring(exclam + 1); + } + if (!fileUrl.getProtocol().equals("file")) { + throw new IllegalArgumentException("Sub-URL of JAR URL is expected to have a scheme of `file`"); + } + return processAsJarPath(toLocalPath(fileUrl), subPath, function); } catch (MalformedURLException e) { throw new RuntimeException("Failed to create a URL for '" + file.substring(0, exclam) + "'", e); } - try (FileSystem jarFs = FileSystems.newFileSystem(jar, (ClassLoader) null)) { - Path localPath = jarFs.getPath("/"); - if (exclam >= 0) { - localPath = localPath.resolve(file.substring(exclam + 1)); - } - return function.apply(localPath); - } catch (IOException e) { - throw new UncheckedIOException("Failed to read " + jar, e); - } } if (FILE.equals(url.getProtocol())) { @@ -154,6 +160,35 @@ public static R processAsPath(URL url, Function function) { throw new IllegalArgumentException("Unexpected protocol " + url.getProtocol() + " for URL " + url); } + private static R processAsJarPath(Path jarPath, String path, Function function) { + try (FileSystem jarFs = JAR_PROVIDER.newFileSystem(jarPath, Map.of())) { + Path localPath = jarFs.getPath("/"); + int start = 0; + for (;;) { + int idx = path.indexOf('!', start); + if (idx == -1) { + return function.apply(localPath.resolve(path)); + } else { + // could be nested JAR? + Path subPath = localPath.resolve(path.substring(0, idx)); + if (Files.isDirectory(subPath)) { + // no, it's a plain directory and the `!` is superfluous + localPath = subPath; + start = idx + 1; + if (start + 1 < path.length() && path.charAt(start + 1) == '/') { + start++; + } + } else { + // yes, it's a nested JAR file + return processAsJarPath(subPath, path.substring(idx + 1), function); + } + } + } + } catch (IOException e) { + throw new UncheckedIOException("Failed to read " + jarPath, e); + } + } + /** * Invokes a consumer providing the input streams to read the content of the URL. * The consumer does not have to close the provided input stream. @@ -204,7 +239,7 @@ public static R readStream(URL url, Function function) throw /** * Translates a URL to local file system path. - * In case the the URL couldn't be translated to a file system path, + * In case the URL couldn't be translated to a file system path, * an instance of {@link IllegalArgumentException} will be thrown. * * @param url URL