From 53cb8ccde24e42488dc6fa5fcfc9dd05bcb6a26b Mon Sep 17 00:00:00 2001 From: Martin Lau Date: Tue, 25 Mar 2014 10:03:39 +1100 Subject: [PATCH] Escape URL characters in JAR URLs Update the spring-boot-loader JarURLConnection class to decode entry names in the same way as the stock JDK class. This allows encoded entry names in the form `%c3%ab` to be loaded. Fixes gh-556 --- .../boot/loader/jar/JarURLConnection.java | 39 ++++++++++++++++++- .../boot/loader/TestJarCreator.java | 3 ++ .../loader/archive/ExplodedArchiveTests.java | 2 +- .../loader/archive/JarFileArchiveTests.java | 2 +- .../boot/loader/jar/JarFileTests.java | 32 +++++++++++++++ 5 files changed, 75 insertions(+), 3 deletions(-) diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java index f7b29edb3d..18eb98848d 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java @@ -16,9 +16,11 @@ package org.springframework.boot.loader.jar; +import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.util.jar.Manifest; @@ -64,7 +66,7 @@ class JarURLConnection extends java.net.JarURLConnection { */ if (separator + SEPARATOR.length() != spec.length()) { this.jarFileUrl = new URL("jar:" + spec); - this.jarEntryName = spec.substring(separator + 2); + this.jarEntryName = decode(spec.substring(separator + 2)); } else { // The root of the archive (!/) @@ -162,4 +164,39 @@ class JarURLConnection extends java.net.JarURLConnection { return builder.toString(); } + private static String decode(String source) { + int length = source.length(); + if ((length == 0) || (source.indexOf('%') < 0)) { + return source; + } + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(length); + for (int i = 0; i < length; i++) { + int ch = source.charAt(i); + if (ch == '%') { + if ((i + 2) >= length) { + throw new IllegalArgumentException("Invalid encoded sequence \"" + + source.substring(i) + "\""); + } + ch = decodeEscapeSequence(source, i); + i += 2; + } + bos.write(ch); + } + return new String(bos.toByteArray(), "UTF-8"); + } + catch (UnsupportedEncodingException ex) { + throw new IllegalStateException(ex); + } + } + + private static char decodeEscapeSequence(String source, int i) { + int hi = Character.digit(source.charAt(i + 1), 16); + int lo = Character.digit(source.charAt(i + 2), 16); + if (hi == -1 || lo == -1) { + throw new IllegalArgumentException("Invalid encoded sequence \"" + + source.substring(i) + "\""); + } + return ((char) ((hi << 4) + lo)); + } } diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java index 4ca5f1e492..2c100efbf9 100644 --- a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java +++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java @@ -43,6 +43,8 @@ public abstract class TestJarCreator { writeEntry(jarOutputStream, "2.dat", 2); writeDirEntry(jarOutputStream, "d/"); writeEntry(jarOutputStream, "d/9.dat", 9); + writeDirEntry(jarOutputStream, "special/"); + writeEntry(jarOutputStream, "special/\u00EB.dat", '\u00EB'); JarEntry nestedEntry = new JarEntry("nested.jar"); byte[] nestedJarData = getNestedJarData(); @@ -68,6 +70,7 @@ public abstract class TestJarCreator { writeManifest(jarOutputStream, "j2"); writeEntry(jarOutputStream, "3.dat", 3); writeEntry(jarOutputStream, "4.dat", 4); + writeEntry(jarOutputStream, "\u00E4.dat", '\u00E4'); jarOutputStream.close(); return byteArrayOutputStream.toByteArray(); } diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java index f086f1878e..4034ceb4f3 100644 --- a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java +++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java @@ -100,7 +100,7 @@ public class ExplodedArchiveTests { @Test public void getEntries() throws Exception { Map entries = getEntriesMap(this.archive); - assertThat(entries.size(), equalTo(7)); + assertThat(entries.size(), equalTo(9)); } @Test diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java index 54af9b90ba..7bd767dcf4 100644 --- a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java +++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java @@ -62,7 +62,7 @@ public class JarFileArchiveTests { @Test public void getEntries() throws Exception { Map entries = getEntriesMap(this.archive); - assertThat(entries.size(), equalTo(7)); + assertThat(entries.size(), equalTo(9)); } @Test diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java index 244dcc3004..e373edc794 100644 --- a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java +++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java @@ -21,6 +21,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.URL; +import java.net.URLClassLoader; import java.util.Enumeration; import java.util.jar.JarEntry; import java.util.jar.Manifest; @@ -50,6 +51,7 @@ import static org.mockito.Mockito.verify; * Tests for {@link JarFile}. * * @author Phillip Webb + * @author Martin Lau */ public class JarFileTests { @@ -70,6 +72,26 @@ public class JarFileTests { this.jarFile = new JarFile(this.rootJarFile); } + @Test + public void jdkJarFile() throws Exception { + // Sanity checks to see how the default jar file operates + java.util.jar.JarFile jarFile = new java.util.jar.JarFile(this.rootJarFile); + Enumeration entries = jarFile.entries(); + assertThat(entries.nextElement().getName(), equalTo("META-INF/")); + assertThat(entries.nextElement().getName(), equalTo("META-INF/MANIFEST.MF")); + assertThat(entries.nextElement().getName(), equalTo("1.dat")); + assertThat(entries.nextElement().getName(), equalTo("2.dat")); + assertThat(entries.nextElement().getName(), equalTo("d/")); + assertThat(entries.nextElement().getName(), equalTo("d/9.dat")); + assertThat(entries.nextElement().getName(), equalTo("special/")); + assertThat(entries.nextElement().getName(), equalTo("special/\u00EB.dat")); + assertThat(entries.nextElement().getName(), equalTo("nested.jar")); + assertThat(entries.hasMoreElements(), equalTo(false)); + URL jarUrl = new URL("jar:" + this.rootJarFile.toURI() + "!/"); + URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { jarUrl }); + assertThat(urlClassLoader.getResource("special/\u00EB.dat"), notNullValue()); + } + @Test public void createFromFile() throws Exception { JarFile jarFile = new JarFile(this.rootJarFile); @@ -99,10 +121,19 @@ public class JarFileTests { assertThat(entries.nextElement().getName(), equalTo("2.dat")); assertThat(entries.nextElement().getName(), equalTo("d/")); assertThat(entries.nextElement().getName(), equalTo("d/9.dat")); + assertThat(entries.nextElement().getName(), equalTo("special/")); + assertThat(entries.nextElement().getName(), equalTo("special/\u00EB.dat")); assertThat(entries.nextElement().getName(), equalTo("nested.jar")); assertThat(entries.hasMoreElements(), equalTo(false)); } + @Test + public void getSpecialResourceViaClassLoader() throws Exception { + URLClassLoader urlClassLoader = new URLClassLoader( + new URL[] { this.jarFile.getUrl() }); + assertThat(urlClassLoader.getResource("special/\u00EB.dat"), notNullValue()); + } + @Test public void getJarEntry() throws Exception { java.util.jar.JarEntry entry = this.jarFile.getJarEntry("1.dat"); @@ -204,6 +235,7 @@ public class JarFileTests { assertThat(entries.nextElement().getName(), equalTo("META-INF/MANIFEST.MF")); assertThat(entries.nextElement().getName(), equalTo("3.dat")); assertThat(entries.nextElement().getName(), equalTo("4.dat")); + assertThat(entries.nextElement().getName(), equalTo("\u00E4.dat")); assertThat(entries.hasMoreElements(), equalTo(false)); InputStream inputStream = nestedJarFile.getInputStream(nestedJarFile