Skip to content

Commit 6cfe880

Browse files
committed
Use StringUtils.uriDecode where feasible
Signed-off-by: Dmytro Nosan <[email protected]>
1 parent 2f91fe2 commit 6cfe880

File tree

9 files changed

+226
-15
lines changed

9 files changed

+226
-15
lines changed

loader/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,10 @@ public String toString() {
235235
return this.string;
236236
}
237237

238+
static String toString(byte[] bytes) {
239+
return new String(bytes, StandardCharsets.UTF_8);
240+
}
241+
238242
static int hashCode(CharSequence charSequence) {
239243
// We're compatible with String's hashCode()
240244
if (charSequence instanceof StringSequence) {

loader/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,18 @@
1616

1717
package org.springframework.boot.loader.jar;
1818

19+
import java.io.ByteArrayOutputStream;
1920
import java.io.FileNotFoundException;
2021
import java.io.IOException;
2122
import java.io.InputStream;
2223
import java.net.MalformedURLException;
2324
import java.net.URL;
2425
import java.net.URLConnection;
26+
import java.net.URLEncoder;
2527
import java.net.URLStreamHandler;
2628
import java.nio.charset.StandardCharsets;
2729
import java.security.Permission;
2830

29-
import org.springframework.util.StringUtils;
30-
3131
/**
3232
* {@link java.net.JarURLConnection} used to support {@link JarFile#getUrl()}.
3333
*
@@ -307,7 +307,41 @@ private StringSequence decode(StringSequence source) {
307307
if (source.isEmpty() || (source.indexOf('%') < 0)) {
308308
return source;
309309
}
310-
return new StringSequence(StringUtils.uriDecode(source.toString(), StandardCharsets.UTF_8));
310+
ByteArrayOutputStream bos = new ByteArrayOutputStream(source.length());
311+
write(source.toString(), bos);
312+
// AsciiBytes is what is used to store the JarEntries so make it symmetric
313+
return new StringSequence(AsciiBytes.toString(bos.toByteArray()));
314+
}
315+
316+
private void write(String source, ByteArrayOutputStream outputStream) {
317+
int length = source.length();
318+
for (int i = 0; i < length; i++) {
319+
int c = source.charAt(i);
320+
if (c > 127) {
321+
String encoded = URLEncoder.encode(String.valueOf((char) c), StandardCharsets.UTF_8);
322+
write(encoded, outputStream);
323+
}
324+
else {
325+
if (c == '%') {
326+
if ((i + 2) >= length) {
327+
throw new IllegalArgumentException(
328+
"Invalid encoded sequence \"" + source.substring(i) + "\"");
329+
}
330+
c = decodeEscapeSequence(source, i);
331+
i += 2;
332+
}
333+
outputStream.write(c);
334+
}
335+
}
336+
}
337+
338+
private char decodeEscapeSequence(String source, int i) {
339+
int hi = Character.digit(source.charAt(i + 1), 16);
340+
int lo = Character.digit(source.charAt(i + 2), 16);
341+
if (hi == -1 || lo == -1) {
342+
throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\"");
343+
}
344+
return ((char) ((hi << 4) + lo));
311345
}
312346

313347
CharSequence toCharSequence() {

loader/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import java.net.URLClassLoader;
2727
import java.net.URLConnection;
2828
import java.net.URLStreamHandler;
29-
import java.nio.charset.StandardCharsets;
3029
import java.security.Permission;
3130
import java.util.Collections;
3231
import java.util.List;
@@ -36,7 +35,7 @@
3635
import java.util.jar.JarFile;
3736

3837
import org.springframework.boot.loader.jar.NestedJarFile;
39-
import org.springframework.util.StringUtils;
38+
import org.springframework.boot.loader.net.util.UrlDecoder;
4039

4140
/**
4241
* {@link java.net.JarURLConnection} alternative to
@@ -342,7 +341,7 @@ static JarUrlConnection open(URL url) throws IOException {
342341
if ("runtime".equals(url.getRef())) {
343342
jarFileUrl = new URL(jarFileUrl, "#runtime");
344343
}
345-
String entryName = StringUtils.uriDecode(spec.substring(separator + 2), StandardCharsets.UTF_8);
344+
String entryName = UrlDecoder.decode(spec.substring(separator + 2));
346345
JarFile jarFile = jarFiles.getOrCreate(true, jarFileUrl);
347346
jarFiles.cacheIfAbsent(true, jarFileUrl, jarFile);
348347
if (!hasEntry(jarFile, entryName)) {

loader/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactory.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,14 @@
2121
import java.io.InputStream;
2222
import java.lang.Runtime.Version;
2323
import java.net.URL;
24-
import java.nio.charset.StandardCharsets;
2524
import java.nio.file.Files;
2625
import java.nio.file.Path;
2726
import java.nio.file.StandardCopyOption;
2827
import java.util.function.Consumer;
2928
import java.util.jar.JarFile;
3029

3130
import org.springframework.boot.loader.net.protocol.nested.NestedLocation;
32-
import org.springframework.util.StringUtils;
31+
import org.springframework.boot.loader.net.util.UrlDecoder;
3332

3433
/**
3534
* Factory used by {@link UrlJarFiles} to create {@link JarFile} instances.
@@ -77,7 +76,7 @@ private boolean isLocal(String host) {
7776

7877
private JarFile createJarFileForLocalFile(URL url, Runtime.Version version, Consumer<JarFile> closeAction)
7978
throws IOException {
80-
String path = StringUtils.uriDecode(url.getPath(), StandardCharsets.UTF_8);
79+
String path = UrlDecoder.decode(url.getPath());
8180
return new UrlJarFile(new File(path), version, closeAction);
8281
}
8382

loader/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,11 @@
1919
import java.io.File;
2020
import java.net.URI;
2121
import java.net.URL;
22-
import java.nio.charset.StandardCharsets;
2322
import java.nio.file.Path;
2423
import java.util.Map;
2524
import java.util.concurrent.ConcurrentHashMap;
2625

27-
import org.springframework.util.StringUtils;
26+
import org.springframework.boot.loader.net.util.UrlDecoder;
2827

2928
/**
3029
* A location obtained from a {@code nested:} {@link URL} consisting of a jar file and an
@@ -76,7 +75,7 @@ public static NestedLocation fromUrl(URL url) {
7675
if (url == null || !"nested".equalsIgnoreCase(url.getProtocol())) {
7776
throw new IllegalArgumentException("'url' must not be null and must use 'nested' protocol");
7877
}
79-
return parse(StringUtils.uriDecode(url.toString().substring(7), StandardCharsets.UTF_8));
78+
return parse(UrlDecoder.decode(url.toString().substring(7)));
8079
}
8180

8281
/**
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.loader.net.util;
18+
19+
import java.nio.ByteBuffer;
20+
import java.nio.CharBuffer;
21+
import java.nio.charset.CharsetDecoder;
22+
import java.nio.charset.CoderResult;
23+
import java.nio.charset.CodingErrorAction;
24+
import java.nio.charset.StandardCharsets;
25+
26+
/**
27+
* Utility to decode URL strings.
28+
*
29+
* @author Phillip Webb
30+
* @since 3.2.0
31+
*/
32+
public final class UrlDecoder {
33+
34+
private UrlDecoder() {
35+
}
36+
37+
/**
38+
* Decode the given string by decoding URL {@code '%'} escapes. This method should be
39+
* identical in behavior to the {@code decode} method in the internal
40+
* {@code sun.net.www.ParseUtil} JDK class.
41+
* @param string the string to decode
42+
* @return the decoded string
43+
*/
44+
public static String decode(String string) {
45+
int length = string.length();
46+
if ((length == 0) || (string.indexOf('%') < 0)) {
47+
return string;
48+
}
49+
StringBuilder result = new StringBuilder(length);
50+
ByteBuffer byteBuffer = ByteBuffer.allocate(length);
51+
CharBuffer charBuffer = CharBuffer.allocate(length);
52+
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder()
53+
.onMalformedInput(CodingErrorAction.REPORT)
54+
.onUnmappableCharacter(CodingErrorAction.REPORT);
55+
int index = 0;
56+
while (index < length) {
57+
char ch = string.charAt(index);
58+
if (ch != '%') {
59+
result.append(ch);
60+
if (index + 1 >= length) {
61+
return result.toString();
62+
}
63+
index++;
64+
continue;
65+
}
66+
index = fillByteBuffer(byteBuffer, string, index, length);
67+
decodeToCharBuffer(byteBuffer, charBuffer, decoder);
68+
result.append(charBuffer.flip());
69+
70+
}
71+
return result.toString();
72+
}
73+
74+
private static int fillByteBuffer(ByteBuffer byteBuffer, String string, int index, int length) {
75+
byteBuffer.clear();
76+
do {
77+
byteBuffer.put(unescape(string, index));
78+
index += 3;
79+
}
80+
while (index < length && string.charAt(index) == '%');
81+
byteBuffer.flip();
82+
return index;
83+
}
84+
85+
private static byte unescape(String string, int index) {
86+
try {
87+
return (byte) Integer.parseInt(string, index + 1, index + 3, 16);
88+
}
89+
catch (NumberFormatException ex) {
90+
throw new IllegalArgumentException();
91+
}
92+
}
93+
94+
private static void decodeToCharBuffer(ByteBuffer byteBuffer, CharBuffer charBuffer, CharsetDecoder decoder) {
95+
decoder.reset();
96+
charBuffer.clear();
97+
assertNoError(decoder.decode(byteBuffer, charBuffer, true));
98+
assertNoError(decoder.flush(charBuffer));
99+
}
100+
101+
private static void assertNoError(CoderResult result) {
102+
if (result.isError()) {
103+
throw new IllegalArgumentException("Error decoding percent encoded characters");
104+
}
105+
}
106+
107+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* Net utilities.
19+
*/
20+
package org.springframework.boot.loader.net.util;

loader/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlTests.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,13 @@
1919
import java.io.File;
2020
import java.net.MalformedURLException;
2121
import java.net.URL;
22-
import java.nio.charset.StandardCharsets;
2322
import java.util.jar.JarEntry;
2423

2524
import org.junit.jupiter.api.BeforeEach;
2625
import org.junit.jupiter.api.Test;
2726
import org.junit.jupiter.api.io.TempDir;
2827

29-
import org.springframework.util.StringUtils;
28+
import org.springframework.boot.loader.net.util.UrlDecoder;
3029

3130
import static org.assertj.core.api.Assertions.assertThat;
3231

@@ -94,7 +93,7 @@ void createWithReservedCharsInName() throws Exception {
9493
setup();
9594
URL url = JarUrl.create(this.jarFile, "lib.jar", "com/example/My.class");
9695
assertThat(url).hasToString("jar:nested:%s/!lib.jar!/com/example/My.class".formatted(this.jarFileUrlPath));
97-
assertThat(StringUtils.uriDecode(url.toString(), StandardCharsets.UTF_8)).contains(badFolderName);
96+
assertThat(UrlDecoder.decode(url.toString())).contains(badFolderName);
9897
}
9998

10099
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.loader.net.util;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import static org.assertj.core.api.Assertions.assertThat;
22+
23+
/**
24+
* Tests for {@link UrlDecoder}.
25+
*
26+
* @author Phillip Webb
27+
*/
28+
class UrlDecoderTests {
29+
30+
@Test
31+
void decodeWhenBasicString() {
32+
assertThat(UrlDecoder.decode("a/b/C.class")).isEqualTo("a/b/C.class");
33+
}
34+
35+
@Test
36+
void decodeWhenHasSingleByteEncodedCharacters() {
37+
assertThat(UrlDecoder.decode("%61/%62/%43.class")).isEqualTo("a/b/C.class");
38+
}
39+
40+
@Test
41+
void decodeWhenHasDoubleByteEncodedCharacters() {
42+
assertThat(UrlDecoder.decode("%c3%a1/b/C.class")).isEqualTo("\u00e1/b/C.class");
43+
}
44+
45+
@Test
46+
void decodeWhenHasMixtureOfEncodedAndUnencodedDoubleByteCharacters() {
47+
assertThat(UrlDecoder.decode("%c3%a1/b/\u00c7.class")).isEqualTo("\u00e1/b/\u00c7.class");
48+
}
49+
50+
}

0 commit comments

Comments
 (0)