From d0ce4daec16f8070267948718237f7ac3d983093 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 2 May 2024 15:50:58 -0700 Subject: [PATCH] Use URI encoded values when creating NestedPath URIs Update `NestedPath.toUri()` so that the URI is constructed using encoded strings. Fixes gh-40615 --- .../boot/loader/nio/file/NestedPath.java | 6 +- .../boot/loader/nio/file/UriPathEncoder.java | 78 +++++++++++++++++++ .../boot/loader/nio/file/NestedPathTests.java | 15 +++- .../loader/nio/file/UriPathEncoderTests.java | 37 +++++++++ 4 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/UriPathEncoder.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/UriPathEncoderTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedPath.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedPath.java index c544a804831..130ca96022a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedPath.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedPath.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -138,9 +138,9 @@ final class NestedPath implements Path { @Override public URI toUri() { try { - String uri = "nested:" + this.fileSystem.getJarPath().toUri().getPath(); + String uri = "nested:" + this.fileSystem.getJarPath().toUri().getRawPath(); if (this.nestedEntryName != null) { - uri += "/!" + this.nestedEntryName; + uri += "/!" + UriPathEncoder.encode(this.nestedEntryName); } return new URI(uri); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/UriPathEncoder.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/UriPathEncoder.java new file mode 100644 index 00000000000..ac2ab05830b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/UriPathEncoder.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.nio.file; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; + +/** + * URL Path Encoder based. + * + * @author Phillip Webb + */ +final class UriPathEncoder { + + // Based on org.springframework.web.util.UriUtils + + private static char[] ALLOWED = "/:@-._~!$&\'()*+,;=".toCharArray(); + + private UriPathEncoder() { + } + + static String encode(String path) { + byte[] bytes = path.getBytes(StandardCharsets.UTF_8); + for (byte b : bytes) { + if (isAllowed(b)) { + return encode(bytes); + } + } + return path; + } + + private static String encode(byte[] bytes) { + ByteArrayOutputStream result = new ByteArrayOutputStream(bytes.length); + for (byte b : bytes) { + if (isAllowed(b)) { + result.write(b); + } + else { + result.write('%'); + result.write(Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16))); + result.write(Character.toUpperCase(Character.forDigit(b & 0xF, 16))); + } + } + return result.toString(StandardCharsets.UTF_8); + } + + private static boolean isAllowed(int ch) { + for (char allowed : ALLOWED) { + if (ch == allowed) { + return true; + } + } + return isAlpha(ch) || isDigit(ch); + } + + private static boolean isAlpha(int ch) { + return (ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z'); + } + + private static boolean isDigit(int ch) { + return (ch >= '0' && ch <= '9'); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedPathTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedPathTests.java index df75d9d930f..22953d0b094 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedPathTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedPathTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -162,7 +162,18 @@ class NestedPathTests { @Test void toUriReturnsUri() throws Exception { - assertThat(this.path.toUri()).isEqualTo(new URI("nested:" + this.jarPath.toUri().getPath() + "/!nested.jar")); + assertThat(this.path.toUri()) + .isEqualTo(new URI("nested:" + this.jarPath.toUri().getRawPath() + "/!nested.jar")); + } + + @Test + void toUriWhenHasSpecialCharsReturnsEncodedUri() throws Exception { + this.jarPath = new File(this.temp, "te st.jar").toPath(); + this.provider = new NestedFileSystemProvider(); + this.fileSystem = new NestedFileSystem(this.provider, this.jarPath); + this.path = new NestedPath(this.fileSystem, "ne sted.jar"); + assertThat(this.path.toUri()) + .isEqualTo(new URI("nested:" + this.jarPath.toUri().getRawPath() + "/!ne%20sted.jar")); } @Test diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/UriPathEncoderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/UriPathEncoderTests.java new file mode 100644 index 00000000000..e0d7179105e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/UriPathEncoderTests.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.nio.file; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link UriPathEncoder}. + * + * @author Phillip Webb + */ +class UriPathEncoderTests { + + @Test + void encodePath() { + assertThat(UriPathEncoder.encode("/foo/bar")).isEqualTo("/foo/bar"); + assertThat(UriPathEncoder.encode("/foo bar")).isEqualTo("/foo%20bar"); + assertThat(UriPathEncoder.encode("/Z\u00fcrich")).isEqualTo("/Z%C3%BCrich"); + } + +}