Skip to content

Commit fcef656

Browse files
pmlopesvietj
authored andcommitted
Fixes #2619: properly handle plus signs when resolving paths (#2623)
* Fixes #2619: properly handle plus signs when resolving paths Signed-off-by: Paulo Lopes <paulo@mlopes.net> * Fixes #2619: properly handle plus signs when resolving paths Signed-off-by: Paulo Lopes <paulo@mlopes.net> * updates based on review Signed-off-by: Paulo Lopes <paulo@mlopes.net> * Delete URIDecoder.java Signed-off-by: Paulo Lopes <paulo@mlopes.net> * Updates based on the review Signed-off-by: Paulo Lopes <paulo@mlopes.net>
1 parent 83e94f6 commit fcef656

7 files changed

Lines changed: 232 additions & 25 deletions

File tree

‎src/main/java/io/vertx/core/impl/FileResolver.java‎

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@
2222
import java.io.File;
2323
import java.io.IOException;
2424
import java.io.InputStream;
25-
import java.io.UnsupportedEncodingException;
2625
import java.net.URL;
27-
import java.net.URLDecoder;
2826
import java.nio.file.FileAlreadyExistsException;
2927
import java.nio.file.Files;
3028
import java.nio.file.StandardCopyOption;
@@ -36,6 +34,8 @@
3634
import java.util.zip.ZipEntry;
3735
import java.util.zip.ZipFile;
3836

37+
import static io.vertx.core.net.impl.URIDecoder.*;
38+
3939
/**
4040
* Sometimes the file resources of an application are bundled into jars, or are somewhere on the classpath but not
4141
* available on the file system, e.g. in the case of a Vert.x webapp bundled as a fat jar.
@@ -162,12 +162,7 @@ private File unpackUrlResource(URL url, String fileName, ClassLoader cl, boolean
162162

163163

164164
private synchronized File unpackFromFileURL(URL url, String fileName, ClassLoader cl) {
165-
File resource;
166-
try {
167-
resource = new File(URLDecoder.decode(url.getPath(), "UTF-8"));
168-
} catch (UnsupportedEncodingException e) {
169-
throw new VertxException(e);
170-
}
165+
final File resource = new File(decodeURIComponent(url.getPath(), false));
171166
boolean isDirectory = resource.isDirectory();
172167
File cacheFile = new File(cacheDir, fileName);
173168
if (!isDirectory) {
@@ -207,7 +202,7 @@ private synchronized File unpackFromJarURL(URL url, String fileName, ClassLoader
207202
idx2 = path.lastIndexOf(".zip!", idx1 - 1);
208203
}
209204
if (idx2 == -1) {
210-
File file = new File(URLDecoder.decode(path.substring(5, idx1 + 4), "UTF-8"));
205+
File file = new File(decodeURIComponent(path.substring(5, idx1 + 4), false));
211206
zip = new ZipFile(file);
212207
} else {
213208
String s = path.substring(idx2 + 6, idx1 + 4);

‎src/main/java/io/vertx/core/impl/verticle/CompilingClassLoader.java‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@
2323
import javax.tools.StandardLocation;
2424
import javax.tools.ToolProvider;
2525
import java.io.File;
26-
import java.io.UnsupportedEncodingException;
2726
import java.net.URL;
28-
import java.net.URLDecoder;
2927
import java.util.ArrayList;
3028
import java.util.Collections;
3129
import java.util.List;
3230

31+
import static io.vertx.core.net.impl.URIDecoder.decodeURIComponent;
32+
3333
/**
3434
*
3535
* Classloader for dynamic .java source file compilation and loading.
@@ -68,12 +68,7 @@ public CompilingClassLoader(ClassLoader loader, String sourceName) {
6868
throw new RuntimeException("Resource not found: " + sourceName);
6969
}
7070
//Need to urldecode it too, since bug in JDK URL class which does not url decode it, so if it contains spaces you are screwed
71-
File sourceFile;
72-
try {
73-
sourceFile = new File(URLDecoder.decode(resource.getFile(), "UTF-8"));
74-
} catch (UnsupportedEncodingException e) {
75-
throw new IllegalStateException("Failed to decode " + e.getMessage());
76-
}
71+
final File sourceFile = new File(decodeURIComponent(resource.getFile(), false));
7772
if (!sourceFile.canRead()) {
7873
throw new RuntimeException("File not found: " + sourceFile.getAbsolutePath() + " current dir is: " + new File(".").getAbsolutePath());
7974
}

‎src/main/java/io/vertx/core/impl/verticle/PackageHelper.java‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,16 @@
1414
import javax.tools.JavaFileObject;
1515
import java.io.File;
1616
import java.io.IOException;
17-
import java.io.UnsupportedEncodingException;
1817
import java.net.JarURLConnection;
1918
import java.net.URI;
2019
import java.net.URL;
21-
import java.net.URLDecoder;
2220
import java.util.ArrayList;
2321
import java.util.Enumeration;
2422
import java.util.List;
2523
import java.util.jar.JarEntry;
2624

25+
import static io.vertx.core.net.impl.URIDecoder.decodeURIComponent;
26+
2727
/**
2828
* @author Janne Hietam&auml;ki
2929
*/
@@ -46,12 +46,7 @@ public List<JavaFileObject> find(String packageName) throws IOException {
4646
while (urlEnumeration.hasMoreElements()) {
4747
URL resource = urlEnumeration.nextElement();
4848
//Need to urldecode it too, since bug in JDK URL class which does not url decode it, so if it contains spaces you are screwed
49-
File directory;
50-
try {
51-
directory = new File(URLDecoder.decode(resource.getFile(), "UTF-8"));
52-
} catch (UnsupportedEncodingException e) {
53-
throw new IllegalStateException("Failed to decode " + e.getMessage());
54-
}
49+
final File directory = new File(decodeURIComponent(resource.getFile(), false));
5550
if (directory.isDirectory()) {
5651
result.addAll(browseDir(packageName, directory));
5752
} else {
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* Copyright (c) 2011-2017 Contributors to the Eclipse Foundation
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7+
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
8+
*
9+
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10+
*/
11+
12+
package io.vertx.core.net.impl;
13+
14+
import java.nio.charset.StandardCharsets;
15+
16+
/**
17+
* @author <a href="mailto:plopes@redhat.com">Paulo Lopes</a>
18+
*/
19+
public final class URIDecoder {
20+
21+
private URIDecoder() {
22+
throw new RuntimeException("Static Class");
23+
}
24+
25+
/**
26+
* Decodes a segment of an URI encoded by a browser.
27+
*
28+
* The string is expected to be encoded as per RFC 3986, Section 2. This is the encoding used by JavaScript functions
29+
* encodeURI and encodeURIComponent, but not escape. For example in this encoding, é (in Unicode U+00E9 or in
30+
* UTF-8 0xC3 0xA9) is encoded as %C3%A9 or %c3%a9.
31+
*
32+
* Plus signs '+' will be handled as spaces and encoded using the default JDK URLEncoder class.
33+
*
34+
* @param s string to decode
35+
*
36+
* @return decoded string
37+
*/
38+
public static String decodeURIComponent(String s) {
39+
return decodeURIComponent(s, true);
40+
}
41+
42+
/**
43+
* Decodes a segment of an URI encoded by a browser.
44+
*
45+
* The string is expected to be encoded as per RFC 3986, Section 2. This is the encoding used by JavaScript functions
46+
* encodeURI and encodeURIComponent, but not escape. For example in this encoding, é (in Unicode U+00E9 or in
47+
* UTF-8 0xC3 0xA9) is encoded as %C3%A9 or %c3%a9.
48+
*
49+
* @param s string to decode
50+
* @param plus weather or not to transform plus signs into spaces
51+
*
52+
* @return decoded string
53+
*/
54+
public static String decodeURIComponent(String s, boolean plus) {
55+
if (s == null) {
56+
return null;
57+
}
58+
59+
final int size = s.length();
60+
boolean modified = false;
61+
int i;
62+
for (i = 0; i < size; i++) {
63+
final char c = s.charAt(i);
64+
if (c == '%' || (plus && c == '+')) {
65+
modified = true;
66+
break;
67+
}
68+
}
69+
if (!modified) {
70+
return s;
71+
}
72+
final byte[] buf = s.getBytes(StandardCharsets.UTF_8);
73+
int pos = i; // position in `buf'.
74+
for (; i < size; i++) {
75+
char c = s.charAt(i);
76+
if (c == '%') {
77+
if (i == size - 1) {
78+
throw new IllegalArgumentException("unterminated escape"
79+
+ " sequence at end of string: " + s);
80+
}
81+
c = s.charAt(++i);
82+
if (c == '%') {
83+
buf[pos++] = '%'; // "%%" -> "%"
84+
break;
85+
}
86+
if (i >= size - 1) {
87+
throw new IllegalArgumentException("partial escape"
88+
+ " sequence at end of string: " + s);
89+
}
90+
c = decodeHexNibble(c);
91+
final char c2 = decodeHexNibble(s.charAt(++i));
92+
if (c == Character.MAX_VALUE || c2 == Character.MAX_VALUE) {
93+
throw new IllegalArgumentException(
94+
"invalid escape sequence `%" + s.charAt(i - 1)
95+
+ s.charAt(i) + "' at index " + (i - 2)
96+
+ " of: " + s);
97+
}
98+
c = (char) (c * 16 + c2);
99+
// shouldn't check for plus since it would be a double decoding
100+
buf[pos++] = (byte) c;
101+
} else {
102+
buf[pos++] = (byte) (plus && c == '+' ? ' ' : c);
103+
}
104+
}
105+
return new String(buf, 0, pos, StandardCharsets.UTF_8);
106+
}
107+
108+
/**
109+
* Helper to decode half of a hexadecimal number from a string.
110+
* @param c The ASCII character of the hexadecimal number to decode.
111+
* Must be in the range {@code [0-9a-fA-F]}.
112+
* @return The hexadecimal value represented in the ASCII character
113+
* given, or {@link Character#MAX_VALUE} if the character is invalid.
114+
*/
115+
private static char decodeHexNibble(final char c) {
116+
if ('0' <= c && c <= '9') {
117+
return (char) (c - '0');
118+
} else if ('a' <= c && c <= 'f') {
119+
return (char) (c - 'a' + 10);
120+
} else if ('A' <= c && c <= 'F') {
121+
return (char) (c - 'A' + 10);
122+
} else {
123+
return Character.MAX_VALUE;
124+
}
125+
}
126+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright (c) 2014 Red Hat, Inc. and others
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7+
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
8+
*
9+
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10+
*/
11+
12+
package io.vertx.core.net.impl;
13+
14+
import org.junit.Test;
15+
16+
import java.net.URLEncoder;
17+
18+
import static io.vertx.core.net.impl.URIDecoder.decodeURIComponent;
19+
import static org.junit.Assert.assertEquals;
20+
import static org.junit.Assert.fail;
21+
22+
/**
23+
* @author <a href="mailto:plopes@redhat.com">Paulo Lopes</a>
24+
*/
25+
public class URIDecoderTest {
26+
27+
@Test
28+
public void testDecode() throws Exception {
29+
String original = "ein verr+++ückter text mit Leerzeichen, Plus und Umlauten";
30+
String encoded = URLEncoder.encode(original, "UTF-8");
31+
assertEquals(original, decodeURIComponent(encoded, true));
32+
}
33+
34+
@Test
35+
public void testPlusAsSpace() {
36+
assertEquals("foo bar", decodeURIComponent("foo+bar"));
37+
}
38+
39+
@Test
40+
public void testPlusAsPlus() {
41+
assertEquals("foo+bar", decodeURIComponent("foo+bar", false));
42+
}
43+
44+
@Test
45+
public void testSpaces() {
46+
assertEquals("foo bar", decodeURIComponent("foo%20bar"));
47+
}
48+
49+
@Test
50+
public void testSingleDecode() {
51+
assertEquals("../blah", decodeURIComponent("%2E%2E%2Fblah"));
52+
assertEquals("%20", decodeURIComponent("%2520"));
53+
}
54+
55+
@Test
56+
public void testFromRFC() {
57+
assertEquals("/ !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", decodeURIComponent("/%20%21%22%23%24%25%26%27%28%29%2A%2B%2C%2D%2E%2F%30%31%32%33%34%35%36%37%38%39%3A%3B%3C%3D%3E%3F%40%41%42%43%44%45%46%47%48%49%4A%4B%4C%4D%4E%4F%50%51%52%53%54%55%56%57%58%59%5A%5B%5C%5D%5E%5F%60%61%62%63%64%65%66%67%68%69%6A%6B%6C%6D%6E%6F%70%71%72%73%74%75%76%77%78%79%7A%7B%7C%7D%7E", false));
58+
}
59+
60+
@Test
61+
public void testNonLatin() {
62+
assertEquals("/foo/ñ/blah/婴儿服饰/eek/ฌ", decodeURIComponent("/foo/%C3%B1/blah/%E5%A9%B4%E5%84%BF%E6%9C%8D%E9%A5%B0/eek/%E0%B8%8C"));
63+
assertEquals("/foo/\u00F1/blah/\u5a74\u513f\u670d\u9970/eek/\u0E0C", decodeURIComponent("/foo/%C3%B1/blah/%E5%A9%B4%E5%84%BF%E6%9C%8D%E9%A5%B0/eek/%E0%B8%8C", false));
64+
}
65+
66+
@Test
67+
public void testIncomplete() {
68+
try {
69+
decodeURIComponent("a%");
70+
fail("should fail");
71+
} catch (RuntimeException e) {
72+
// expected
73+
}
74+
}
75+
76+
@Test
77+
public void testCaseInsensitive() {
78+
assertEquals("../blah", decodeURIComponent("%2e%2e%2fblah"));
79+
}
80+
81+
}

‎src/test/java/io/vertx/test/core/FileSystemFileResolverTest.java‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@
1111

1212
package io.vertx.test.core;
1313

14+
import org.junit.Test;
15+
16+
import java.io.File;
17+
18+
import org.junit.Test;
19+
20+
import java.io.File;
21+
1422
/**
1523
* @author <a href="http://tfox.org">Tim Fox</a>
1624
*/
@@ -22,4 +30,10 @@ public void setUp() throws Exception {
2230
webRoot = "webroot";
2331
}
2432

33+
@Test
34+
public void testResolvePlusSignsOnName() {
35+
File file = resolver.resolveFile("this+that");
36+
assertFalse(file.exists());
37+
assertEquals("this+that", file.getPath());
38+
}
2539
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
hi!

0 commit comments

Comments
 (0)