Skip to content

Commit b8388e3

Browse files
committed
Merge pull request #46751 from nosan
* pr/46751: Polish "Use StringUtils.uriDecode where feasible" Use StringUtils.uriDecode where feasible Closes gh-46751
2 parents a176849 + 203f14e commit b8388e3

File tree

3 files changed

+63
-108
lines changed

3 files changed

+63
-108
lines changed

loader/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/util/UrlDecoder.java

Lines changed: 60 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,16 @@
1616

1717
package org.springframework.boot.loader.net.util;
1818

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;
19+
import java.nio.charset.Charset;
2420
import java.nio.charset.StandardCharsets;
21+
import java.util.HexFormat;
2522

2623
/**
27-
* Utility to decode URL strings.
24+
* Utility to decode URL strings. Copied frm Spring Framework's {@code StringUtils} as we
25+
* cannot depend on it in the loader.
2826
*
2927
* @author Phillip Webb
28+
* @author Stephane Nicoll
3029
* @since 3.2.0
3130
*/
3231
public final class UrlDecoder {
@@ -35,73 +34,69 @@ private UrlDecoder() {
3534
}
3635

3736
/**
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
37+
* Decode the given encoded URI component value by replacing each "<i>{@code %xy}</i>"
38+
* sequence with a hexadecimal representation of the character in
39+
* {@link StandardCharsets#UTF_8 UTF-8}, leaving other characters unmodified.
40+
* @param source the encoded URI component value
41+
* @return the decoded value
4342
*/
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();
43+
public static String decode(String source) {
44+
return decode(source, StandardCharsets.UTF_8);
7245
}
7346

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;
47+
/**
48+
* Decode the given encoded URI component value by replacing each "<i>{@code %xy}</i>"
49+
* sequence with a hexadecimal representation of the character in the specified
50+
* character encoding, leaving other characters unmodified.
51+
* @param source the encoded URI component value
52+
* @param charset the character encoding to use to decode the "<i>{@code %xy}</i>"
53+
* sequences
54+
* @return the decoded value
55+
*/
56+
public static String decode(String source, Charset charset) {
57+
int length = source.length();
58+
int firstPercentIndex = source.indexOf('%');
59+
if (length == 0 || firstPercentIndex < 0) {
60+
return source;
7961
}
80-
while (index < length && string.charAt(index) == '%');
81-
byteBuffer.flip();
82-
return index;
83-
}
8462

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-
}
63+
StringBuilder output = new StringBuilder(length);
64+
output.append(source, 0, firstPercentIndex);
65+
byte[] bytes = null;
66+
int i = firstPercentIndex;
67+
while (i < length) {
68+
char ch = source.charAt(i);
69+
if (ch == '%') {
70+
try {
71+
if (bytes == null) {
72+
bytes = new byte[(length - i) / 3];
73+
}
9374

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-
}
75+
int pos = 0;
76+
while (i + 2 < length && ch == '%') {
77+
bytes[pos++] = (byte) HexFormat.fromHexDigits(source, i + 1, i + 3);
78+
i += 3;
79+
if (i < length) {
80+
ch = source.charAt(i);
81+
}
82+
}
83+
84+
if (i < length && ch == '%') {
85+
throw new IllegalArgumentException("Incomplete trailing escape (%) pattern");
86+
}
10087

101-
private static void assertNoError(CoderResult result) {
102-
if (result.isError()) {
103-
throw new IllegalArgumentException("Error decoding percent encoded characters");
88+
output.append(new String(bytes, 0, pos, charset));
89+
}
90+
catch (NumberFormatException ex) {
91+
throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\"");
92+
}
93+
}
94+
else {
95+
output.append(ch);
96+
i++;
97+
}
10498
}
99+
return output.toString();
105100
}
106101

107102
}

module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryResourceAttributes.java

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package org.springframework.boot.opentelemetry.autoconfigure;
1818

19-
import java.io.ByteArrayOutputStream;
2019
import java.nio.charset.StandardCharsets;
2120
import java.util.Collections;
2221
import java.util.LinkedHashMap;
@@ -129,7 +128,7 @@ private Map<String, String> getResourceAttributesFromEnv() {
129128
if (index > 0) {
130129
String key = attribute.substring(0, index);
131130
String value = attribute.substring(index + 1);
132-
attributes.put(key.trim(), decode(value.trim()));
131+
attributes.put(key.trim(), StringUtils.uriDecode(value.trim(), StandardCharsets.UTF_8));
133132
}
134133
}
135134
String otelServiceName = getEnv("OTEL_SERVICE_NAME");
@@ -143,43 +142,4 @@ private Map<String, String> getResourceAttributesFromEnv() {
143142
return this.systemEnvironment.apply(name);
144143
}
145144

146-
/**
147-
* Decodes a percent-encoded string. Converts sequences like '%HH' (where HH
148-
* represents hexadecimal digits) back into their literal representations.
149-
* <p>
150-
* Inspired by {@code org.apache.commons.codec.net.PercentCodec}.
151-
* @param value value to decode
152-
* @return the decoded string
153-
*/
154-
private static String decode(String value) {
155-
if (value.indexOf('%') < 0) {
156-
return value;
157-
}
158-
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
159-
ByteArrayOutputStream out = new ByteArrayOutputStream(bytes.length);
160-
for (int i = 0; i < bytes.length; i++) {
161-
byte b = bytes[i];
162-
if (b != '%') {
163-
out.write(b);
164-
continue;
165-
}
166-
int u = decodeHex(bytes, i + 1);
167-
int l = decodeHex(bytes, i + 2);
168-
if (u >= 0 && l >= 0) {
169-
out.write((u << 4) + l);
170-
}
171-
else {
172-
throw new IllegalArgumentException(
173-
"Failed to decode percent-encoded characters at index %d in the value: '%s'".formatted(i,
174-
value));
175-
}
176-
i += 2;
177-
}
178-
return out.toString(StandardCharsets.UTF_8);
179-
}
180-
181-
private static int decodeHex(byte[] bytes, int index) {
182-
return (index < bytes.length) ? Character.digit(bytes[index], 16) : -1;
183-
}
184-
185145
}

module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryResourceAttributesTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ void otelResourceAttributeValuesShouldBePercentDecodedWhenMultiByteSequences() {
137137
void illegalArgumentExceptionShouldBeThrownWhenDecodingIllegalHexCharPercentEncodedValue() {
138138
this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", "key=abc%ß");
139139
assertThatIllegalArgumentException().isThrownBy(this::getAttributes)
140-
.withMessage("Failed to decode percent-encoded characters at index 3 in the value: 'abc%ß'");
140+
.withMessage("Incomplete trailing escape (%) pattern");
141141
}
142142

143143
@Test
@@ -150,7 +150,7 @@ void replacementCharShouldBeUsedWhenDecodingNonUtf8Character() {
150150
void illegalArgumentExceptionShouldBeThrownWhenDecodingInvalidPercentEncodedValue() {
151151
this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", "key=%");
152152
assertThatIllegalArgumentException().isThrownBy(this::getAttributes)
153-
.withMessage("Failed to decode percent-encoded characters at index 0 in the value: '%'");
153+
.withMessage("Incomplete trailing escape (%) pattern");
154154
}
155155

156156
@Test

0 commit comments

Comments
 (0)