diff --git a/loader/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/util/UrlDecoder.java b/loader/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/util/UrlDecoder.java index f455a49a273..bc8a3750d59 100644 --- a/loader/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/util/UrlDecoder.java +++ b/loader/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/util/UrlDecoder.java @@ -16,17 +16,16 @@ package org.springframework.boot.loader.net.util; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.CharsetDecoder; -import java.nio.charset.CoderResult; -import java.nio.charset.CodingErrorAction; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.HexFormat; /** - * Utility to decode URL strings. + * Utility to decode URL strings. Copied frm Spring Framework's {@code StringUtils} as we + * cannot depend on it in the loader. * * @author Phillip Webb + * @author Stephane Nicoll * @since 3.2.0 */ public final class UrlDecoder { @@ -35,73 +34,69 @@ public final class UrlDecoder { } /** - * Decode the given string by decoding URL {@code '%'} escapes. This method should be - * identical in behavior to the {@code decode} method in the internal - * {@code sun.net.www.ParseUtil} JDK class. - * @param string the string to decode - * @return the decoded string + * Decode the given encoded URI component value by replacing each "{@code %xy}" + * sequence with a hexadecimal representation of the character in + * {@link StandardCharsets#UTF_8 UTF-8}, leaving other characters unmodified. + * @param source the encoded URI component value + * @return the decoded value */ - public static String decode(String string) { - int length = string.length(); - if ((length == 0) || (string.indexOf('%') < 0)) { - return string; + public static String decode(String source) { + return decode(source, StandardCharsets.UTF_8); + } + + /** + * Decode the given encoded URI component value by replacing each "{@code %xy}" + * sequence with a hexadecimal representation of the character in the specified + * character encoding, leaving other characters unmodified. + * @param source the encoded URI component value + * @param charset the character encoding to use to decode the "{@code %xy}" + * sequences + * @return the decoded value + */ + public static String decode(String source, Charset charset) { + int length = source.length(); + int firstPercentIndex = source.indexOf('%'); + if (length == 0 || firstPercentIndex < 0) { + return source; } - StringBuilder result = new StringBuilder(length); - ByteBuffer byteBuffer = ByteBuffer.allocate(length); - CharBuffer charBuffer = CharBuffer.allocate(length); - CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder() - .onMalformedInput(CodingErrorAction.REPORT) - .onUnmappableCharacter(CodingErrorAction.REPORT); - int index = 0; - while (index < length) { - char ch = string.charAt(index); - if (ch != '%') { - result.append(ch); - if (index + 1 >= length) { - return result.toString(); + + StringBuilder output = new StringBuilder(length); + output.append(source, 0, firstPercentIndex); + byte[] bytes = null; + int i = firstPercentIndex; + while (i < length) { + char ch = source.charAt(i); + if (ch == '%') { + try { + if (bytes == null) { + bytes = new byte[(length - i) / 3]; + } + + int pos = 0; + while (i + 2 < length && ch == '%') { + bytes[pos++] = (byte) HexFormat.fromHexDigits(source, i + 1, i + 3); + i += 3; + if (i < length) { + ch = source.charAt(i); + } + } + + if (i < length && ch == '%') { + throw new IllegalArgumentException("Incomplete trailing escape (%) pattern"); + } + + output.append(new String(bytes, 0, pos, charset)); + } + catch (NumberFormatException ex) { + throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\""); } - index++; - continue; } - index = fillByteBuffer(byteBuffer, string, index, length); - decodeToCharBuffer(byteBuffer, charBuffer, decoder); - result.append(charBuffer.flip()); - - } - return result.toString(); - } - - private static int fillByteBuffer(ByteBuffer byteBuffer, String string, int index, int length) { - byteBuffer.clear(); - do { - byteBuffer.put(unescape(string, index)); - index += 3; - } - while (index < length && string.charAt(index) == '%'); - byteBuffer.flip(); - return index; - } - - private static byte unescape(String string, int index) { - try { - return (byte) Integer.parseInt(string, index + 1, index + 3, 16); - } - catch (NumberFormatException ex) { - throw new IllegalArgumentException(); - } - } - - private static void decodeToCharBuffer(ByteBuffer byteBuffer, CharBuffer charBuffer, CharsetDecoder decoder) { - decoder.reset(); - charBuffer.clear(); - assertNoError(decoder.decode(byteBuffer, charBuffer, true)); - assertNoError(decoder.flush(charBuffer)); - } - - private static void assertNoError(CoderResult result) { - if (result.isError()) { - throw new IllegalArgumentException("Error decoding percent encoded characters"); + else { + output.append(ch); + i++; + } } + return output.toString(); } } diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryResourceAttributes.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryResourceAttributes.java index a8c2bf095d5..a0a0c44dc8a 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryResourceAttributes.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryResourceAttributes.java @@ -16,7 +16,6 @@ package org.springframework.boot.opentelemetry.autoconfigure; -import java.io.ByteArrayOutputStream; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.LinkedHashMap; @@ -129,7 +128,7 @@ public class OpenTelemetryResourceAttributes { if (index > 0) { String key = attribute.substring(0, index); String value = attribute.substring(index + 1); - attributes.put(key.trim(), decode(value.trim())); + attributes.put(key.trim(), StringUtils.uriDecode(value.trim(), StandardCharsets.UTF_8)); } } String otelServiceName = getEnv("OTEL_SERVICE_NAME"); @@ -143,43 +142,4 @@ public class OpenTelemetryResourceAttributes { return this.systemEnvironment.apply(name); } - /** - * Decodes a percent-encoded string. Converts sequences like '%HH' (where HH - * represents hexadecimal digits) back into their literal representations. - *
- * Inspired by {@code org.apache.commons.codec.net.PercentCodec}. - * @param value value to decode - * @return the decoded string - */ - private static String decode(String value) { - if (value.indexOf('%') < 0) { - return value; - } - byte[] bytes = value.getBytes(StandardCharsets.UTF_8); - ByteArrayOutputStream out = new ByteArrayOutputStream(bytes.length); - for (int i = 0; i < bytes.length; i++) { - byte b = bytes[i]; - if (b != '%') { - out.write(b); - continue; - } - int u = decodeHex(bytes, i + 1); - int l = decodeHex(bytes, i + 2); - if (u >= 0 && l >= 0) { - out.write((u << 4) + l); - } - else { - throw new IllegalArgumentException( - "Failed to decode percent-encoded characters at index %d in the value: '%s'".formatted(i, - value)); - } - i += 2; - } - return out.toString(StandardCharsets.UTF_8); - } - - private static int decodeHex(byte[] bytes, int index) { - return (index < bytes.length) ? Character.digit(bytes[index], 16) : -1; - } - } diff --git a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryResourceAttributesTests.java b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryResourceAttributesTests.java index 7d5d245bdf0..ebae139464a 100644 --- a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryResourceAttributesTests.java +++ b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetryResourceAttributesTests.java @@ -137,7 +137,7 @@ class OpenTelemetryResourceAttributesTests { void illegalArgumentExceptionShouldBeThrownWhenDecodingIllegalHexCharPercentEncodedValue() { this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", "key=abc%ß"); assertThatIllegalArgumentException().isThrownBy(this::getAttributes) - .withMessage("Failed to decode percent-encoded characters at index 3 in the value: 'abc%ß'"); + .withMessage("Incomplete trailing escape (%) pattern"); } @Test @@ -150,7 +150,7 @@ class OpenTelemetryResourceAttributesTests { void illegalArgumentExceptionShouldBeThrownWhenDecodingInvalidPercentEncodedValue() { this.environmentVariables.put("OTEL_RESOURCE_ATTRIBUTES", "key=%"); assertThatIllegalArgumentException().isThrownBy(this::getAttributes) - .withMessage("Failed to decode percent-encoded characters at index 0 in the value: '%'"); + .withMessage("Incomplete trailing escape (%) pattern"); } @Test