Skip to content

Commit

Permalink
Handle paths with a question mark in them
Browse files Browse the repository at this point in the history
  • Loading branch information
stuartwdouglas committed Feb 16, 2022
1 parent 2e7e5c9 commit 07bb261
Show file tree
Hide file tree
Showing 9 changed files with 233 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.io.InterruptedIOException;
import java.io.UncheckedIOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.nio.channels.ClosedChannelException;
import java.nio.file.Files;
Expand Down Expand Up @@ -208,7 +209,25 @@ public URL getUrl() {
lock.readLock().lock();
try {
if (pathTree.isOpen()) {
return url = path.toUri().toURL();
URI uri = path.toUri();
URL url;
// the URLClassLoader doesn't add trailing slashes to directories, so we make sure we return
// the same URL as it would to avoid having QuarkusClassLoader return different URLs
// (one with a trailing slash and one without) for same resource
if (uri.getPath() != null && uri.getPath().endsWith("/")) {
String uriStr = uri.toString();
url = new URL(uriStr.substring(0, uriStr.length() - 1));
} else {
url = uri.toURL();
}
if (url.getQuery() != null) {
//huge hack to work around a JDK bug. ZipPath does not escape properly, so if there is a ?
//in the path then it ends up in the query string and everything is screwy.
//if there are multiple question marks the extra ones end up in the path and not the query string as you would expect
url = new URL(url.getProtocol(), url.getHost(), url.getPort(),
url.getPath().replaceAll("\\?", "%3F") + "%3F" + url.getQuery());
}
return this.url = url;
}
return url = apply(tree -> tree.apply(name, visit -> visit == null ? null : visit.getUrl()));
} catch (MalformedURLException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package io.quarkus.bootstrap.classloader;

import io.quarkus.bootstrap.classloading.ClassPathElement;
import io.quarkus.bootstrap.classloading.QuarkusClassLoader;
import io.quarkus.bootstrap.util.IoUtils;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.exporter.ExplodedExporter;
import org.jboss.shrinkwrap.api.exporter.ZipExporter;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

public class ClassLoadingPathTreeResourceUrlTestCase {

/**
* URLClassLoader will return URL's that end with a / if the call to getResource ends with a /
*/
@ParameterizedTest
@MethodSource("paths")
public void testUrlReturnedFromClassLoaderDirectory(String testPath) throws Exception {
JavaArchive jar = ShrinkWrap.create(JavaArchive.class)
.add(new StringAsset("a"), "a.txt")
.add(new StringAsset("b"), "b/b.txt");
Path path = Files.createTempDirectory(testPath);
try {
jar.as(ExplodedExporter.class).exportExploded(path.toFile(), "tmp");

ClassLoader cl = QuarkusClassLoader.builder("test", getClass().getClassLoader(), false)
.addElement(ClassPathElement.fromPath(path.resolve("tmp"), true))
.build();
URL res = cl.getResource("a.txt");
Assertions.assertNotNull(res);
Assertions.assertNull(res.getQuery());
res = cl.getResource("a.txt/");
Assertions.assertNull(res);

res = cl.getResource("b");
Assertions.assertNotNull(res);
Assertions.assertFalse(res.toExternalForm().endsWith("/"));

res = cl.getResource("b/");
Assertions.assertNotNull(res);
Assertions.assertTrue(res.toExternalForm().endsWith("/"));

} finally {
IoUtils.recursiveDelete(path);
}
}

/**
* Test that {@link QuarkusClassLoader#getResourceAsStream(String)} returns a stream for directory
* resources
*
* @throws Exception
* @see <a href="https://github.com/quarkusio/quarkus/issues/11707"/>
*/
@ParameterizedTest
@MethodSource("paths")
public void testResourceAsStreamForDirectory(String testPath) throws Exception {
final JavaArchive jar = ShrinkWrap.create(JavaArchive.class)
.add(new StringAsset("a"), "a.txt")
.add(new StringAsset("b"), "b/b.txt");
final Path tmpDir = Files.createTempDirectory(testPath);
try {
jar.as(ExplodedExporter.class).exportExploded(tmpDir.toFile(), "tmpcltest");
final ClassLoader cl = QuarkusClassLoader.builder("test", getClass().getClassLoader(), false)
.addElement(ClassPathElement.fromPath(tmpDir.resolve("tmpcltest"), true))
.build();

try (final InputStream is = cl.getResourceAsStream("b/")) {
Assertions.assertNotNull(is, "InputStream is null for a directory resource");
}
try (final InputStream is = cl.getResourceAsStream("b")) {
Assertions.assertNotNull(is, "InputStream is null for a directory resource");
}
} finally {
IoUtils.recursiveDelete(tmpDir);
}
}

/**
* URLClassLoader will return URL's that end with a / if the call to getResource ends with a /
*/
@ParameterizedTest
@MethodSource("paths")
public void testUrlReturnedFromClassLoaderJarFile(String testPath) throws Exception {
JavaArchive jar = ShrinkWrap.create(JavaArchive.class)
.add(new StringAsset("a"), "a.txt")
.add(new StringAsset("b"), "b/b.txt");
Path path = Files.createTempFile(testPath, "quarkus-test.jar");
try {
jar.as(ZipExporter.class).exportTo(path.toFile(), true);

ClassLoader cl = QuarkusClassLoader.builder("test", getClass().getClassLoader(), false)
.addElement(ClassPathElement.fromPath(path, true))
.build();
URL res = cl.getResource("a.txt");
Assertions.assertNotNull(res);
Assertions.assertNull(res.getQuery());
res = cl.getResource("a.txt/");
Assertions.assertNull(res);

res = cl.getResource("b");
Assertions.assertNotNull(res);
Assertions.assertFalse(res.toExternalForm().endsWith("/"));

res = cl.getResource("b/");
Assertions.assertNotNull(res);
Assertions.assertTrue(res.toExternalForm().endsWith("/"));

} finally {
IoUtils.recursiveDelete(path);
}
}

public static Iterable<Object[]> paths() {
if (OS.WINDOWS.isCurrentOs()) {
return List.<Object[]> of(new Object[] { "test" });
}
return List.<Object[]> of(new Object[] { "test" }, new Object[] { "dir?with?question?mark" });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,31 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.exporter.ExplodedExporter;
import org.jboss.shrinkwrap.api.exporter.ZipExporter;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

//see https://github.com/quarkusio/quarkus/issues/10943
public class ClassLoadingResourceUrlTestCase {

/**
* URLClassLoader will return URL's that end with a / if the call to getResource ends with a /
*/
@Test
public void testUrlReturnedFromClassLoaderDirectory() throws Exception {
@ParameterizedTest
@MethodSource("paths")
public void testUrlReturnedFromClassLoaderDirectory(String testPath) throws Exception {
JavaArchive jar = ShrinkWrap.create(JavaArchive.class)
.add(new StringAsset("a"), "a.txt")
.add(new StringAsset("b"), "b/b.txt");
Path path = Files.createTempDirectory("test");
Path path = Files.createTempDirectory(testPath);
try {
jar.as(ExplodedExporter.class).exportExploded(path.toFile(), "tmp");

Expand All @@ -40,6 +45,7 @@ public void testUrlReturnedFromClassLoaderDirectory() throws Exception {
.build();
URL res = cl.getResource("a.txt");
Assertions.assertNotNull(res);
Assertions.assertNull(res.getQuery());
res = cl.getResource("a.txt/");
Assertions.assertNull(res);

Expand All @@ -63,12 +69,13 @@ public void testUrlReturnedFromClassLoaderDirectory() throws Exception {
* @throws Exception
* @see <a href="https://github.com/quarkusio/quarkus/issues/11707"/>
*/
@Test
public void testResourceAsStreamForDirectory() throws Exception {
@ParameterizedTest
@MethodSource("paths")
public void testResourceAsStreamForDirectory(String testPath) throws Exception {
final JavaArchive jar = ShrinkWrap.create(JavaArchive.class)
.add(new StringAsset("a"), "a.txt")
.add(new StringAsset("b"), "b/b.txt");
final Path tmpDir = Files.createTempDirectory("test");
final Path tmpDir = Files.createTempDirectory(testPath);
try {
jar.as(ExplodedExporter.class).exportExploded(tmpDir.toFile(), "tmpcltest");
final ClassLoader cl = QuarkusClassLoader.builder("test", getClass().getClassLoader(), false)
Expand All @@ -89,12 +96,13 @@ public void testResourceAsStreamForDirectory() throws Exception {
/**
* URLClassLoader will return URL's that end with a / if the call to getResource ends with a /
*/
@Test
public void testUrlReturnedFromClassLoaderJarFile() throws Exception {
@ParameterizedTest
@MethodSource("paths")
public void testUrlReturnedFromClassLoaderJarFile(String testPath) throws Exception {
JavaArchive jar = ShrinkWrap.create(JavaArchive.class)
.add(new StringAsset("a"), "a.txt")
.add(new StringAsset("b"), "b/b.txt");
Path path = Files.createTempFile("test", "quarkus-test.jar");
Path path = Files.createTempFile(testPath, "quarkus-test.jar");
try {
jar.as(ZipExporter.class).exportTo(path.toFile(), true);

Expand All @@ -103,6 +111,7 @@ public void testUrlReturnedFromClassLoaderJarFile() throws Exception {
.build();
URL res = cl.getResource("a.txt");
Assertions.assertNotNull(res);
Assertions.assertNull(res.getQuery());
res = cl.getResource("a.txt/");
Assertions.assertNull(res);

Expand Down Expand Up @@ -137,4 +146,11 @@ public void testMemoryUrlConnections() throws Exception {
Assertions.assertEquals("hello", new String(urlConnection.getInputStream().readAllBytes(), StandardCharsets.UTF_8));

}

public static Iterable<Object[]> paths() {
if (OS.WINDOWS.isCurrentOs()) {
return List.<Object[]> of(new Object[] { "test" });
}
return List.<Object[]> of(new Object[] { "test" }, new Object[] { "dir?with?question?mark" });
}
}
2 changes: 1 addition & 1 deletion independent-projects/bootstrap/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
<plexus-classworlds.version>2.6.0</plexus-classworlds.version> <!-- not actually used but ClassRealm class is referenced from the API used in BootstrapWagonConfigurator -->
<smallrye-common.version>1.9.0</smallrye-common.version>
<gradle-tooling.version>7.4</gradle-tooling.version>
<quarkus-fs-util.version>0.0.8</quarkus-fs-util.version>
<quarkus-fs-util.version>0.0.9</quarkus-fs-util.version>
</properties>
<modules>
<module>bom</module>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,10 @@ public URL getResourceURL(String resource) {
// for the "path" which includes the "realName"
final URL resUrl = new URI(jarUri.getScheme(), jarUri.getPath() + "!/" + realName, null).toURL();
// wrap it up into a "jar" protocol URL
return new URL("jar", null, resUrl.getProtocol() + ':' + resUrl.getPath());
//horrible hack to deal with '?' characters in the URL
//seems to be the only way, the URI constructor just does not let you handle them in a sane way
return new URL("jar", null, resUrl.getProtocol() + ':' + resUrl.getPath()
+ (resUrl.getQuery() == null ? "" : ("%3F" + resUrl.getQuery())));
} catch (MalformedURLException | URISyntaxException e) {
throw new RuntimeException(e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.OS;

/**
* Tests {@link JarResource}
Expand All @@ -25,12 +27,31 @@ public class JarResourceTest {
*/
@Test
public void testResourceURLEncoding() throws Exception {
testInternal("test");
}

/**
* As above but with a question mark in the path
* <p>
* Disabled on windows as such paths are not allowed
*/
@Test
@DisabledOnOs(OS.WINDOWS)
public void testResourceURLEncodingWithQuestionMark() throws Exception {
testInternal("test?dir");
}

/**
* Tests that the URL(s) returned from {@link JarResource#getResourceURL(String)} are properly encoded and can be used
* to open connection to the URL to read data
*/
private void testInternal(String path) throws Exception {
final JavaArchive jar = ShrinkWrap.create(JavaArchive.class);
final String[] files = new String[] { "a.txt", "a b.txt", ",;~!@#$%^&().txt" };
for (final String file : files) {
jar.add(new StringAsset("hello"), file);
}
final Path testDir = Files.createTempDirectory("test");
final Path testDir = Files.createTempDirectory(path);
// create a child dir with special characters
final Path specialCharDir = Files.createDirectory(Paths.get(testDir.toString(), ",;~!@#$%^&()"));
final Path jarFilePath = Files.createTempFile(specialCharDir, "test", "quarkus-test.jar");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
public class LegacyJarQuarkusIntegrationTestIT extends QuarkusITBase {

@Test
public void testFastJar() throws MavenInvocationException, IOException {
public void testLegacyJar() throws MavenInvocationException, IOException {
doTest("qit-legacy-jar", "legacyjar");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.quarkus.maven.it;

import java.io.IOException;

import org.apache.maven.shared.invoker.MavenInvocationException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.OS;

@DisableForNative
@DisabledOnOs(OS.WINDOWS)
public class QuestionMarkInPathIntegrationTestIT extends QuarkusITBase {

@Test
public void testFastJar() throws MavenInvocationException, IOException {
doTest("qit?fast?jar", "fastjar");
}

@Test
public void testLegacyJar() throws MavenInvocationException, IOException {
doTest("qit?legacy?jar", "legacyjar");
}

@Test
public void testUberJar() throws MavenInvocationException, IOException {
doTest("qit?uber?jar", "uberjar");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
public class UberJarQuarkusIntegrationTestIT extends QuarkusITBase {

@Test
public void testFastJar() throws MavenInvocationException, IOException {
public void testUberJar() throws MavenInvocationException, IOException {
doTest("qit-uber-jar", "uberjar");
}
}

0 comments on commit 07bb261

Please sign in to comment.