From 7708ec75927653e78ee87c2d13582785b8955348 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 8 May 2024 15:34:32 -0700 Subject: [PATCH] Fix Windows path handling for nested jars Update `Path` creation for nested locations to allow both UNC and classic file references to be used. This commit attempts to align our URL handling with that of standard file URLs. The `NestedLocation` class no longer attempts to remove leading all `\` characters and instead only removes the first `\` when the second char is `:`. This duplicates the logic found in Java's own internal `WindowsUriSupport` class which is used when calling `Path.of(url)` with a `file:` URL. Fixes gh-40549 --- .../boot/loader/launch/Archive.java | 8 ++-- .../net/protocol/nested/NestedLocation.java | 40 ++++++++++--------- .../protocol/nested/NestedUrlConnection.java | 3 +- .../protocol/nested/NestedLocationTests.java | 2 +- 4 files changed, 28 insertions(+), 25 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/Archive.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/Archive.java index 933a630ffb30..f78a78a8d1e0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/Archive.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/Archive.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. @@ -20,6 +20,7 @@ import java.io.IOException; import java.net.URI; import java.net.URL; +import java.nio.file.Path; import java.security.CodeSource; import java.security.ProtectionDomain; import java.util.Set; @@ -107,11 +108,10 @@ static Archive create(Class target) throws Exception { static Archive create(ProtectionDomain protectionDomain) throws Exception { CodeSource codeSource = protectionDomain.getCodeSource(); URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null; - String path = (location != null) ? location.getSchemeSpecificPart() : null; - if (path == null) { + if (location == null) { throw new IllegalStateException("Unable to determine code source archive"); } - return create(new File(path)); + return create(Path.of(location).toFile()); } /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java index 94e513b64036..f99e13116a7c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java @@ -53,7 +53,9 @@ */ public record NestedLocation(Path path, String nestedEntryName) { - private static final Map cache = new ConcurrentHashMap<>(); + private static final Map locationCache = new ConcurrentHashMap<>(); + + private static final Map pathCache = new ConcurrentHashMap<>(); public NestedLocation(Path path, String nestedEntryName) { if (path == null) { @@ -89,35 +91,37 @@ public static NestedLocation fromUri(URI uri) { return parse(uri.getSchemeSpecificPart()); } - static NestedLocation parse(String path) { - if (path == null || path.isEmpty()) { - throw new IllegalArgumentException("'path' must not be empty"); + static NestedLocation parse(String location) { + if (location == null || location.isEmpty()) { + throw new IllegalArgumentException("'location' must not be empty"); } - int index = path.lastIndexOf("/!"); - return cache.computeIfAbsent(path, (l) -> create(index, l)); + return locationCache.computeIfAbsent(location, (key) -> create(location)); } - private static NestedLocation create(int index, String location) { + private static NestedLocation create(String location) { + int index = location.lastIndexOf("/!"); String locationPath = (index != -1) ? location.substring(0, index) : location; - if (isWindows() && !isUncPath(location)) { - while (locationPath.startsWith("/")) { - locationPath = locationPath.substring(1, locationPath.length()); - } - } String nestedEntryName = (index != -1) ? location.substring(index + 2) : null; - return new NestedLocation((!locationPath.isEmpty()) ? Path.of(locationPath) : null, nestedEntryName); + return new NestedLocation((!locationPath.isEmpty()) ? asPath(locationPath) : null, nestedEntryName); } - private static boolean isWindows() { - return File.separatorChar == '\\'; + private static Path asPath(String locationPath) { + return pathCache.computeIfAbsent(locationPath, (key) -> { + if (isWindows() && locationPath.length() > 2 && locationPath.charAt(2) == ':') { + // Use the same logic as Java's internal WindowsUriSupport class + return Path.of(locationPath.substring(1)); + } + return Path.of(locationPath); + }); } - private static boolean isUncPath(String input) { - return !input.contains(":"); + private static boolean isWindows() { + return File.separatorChar == '\\'; } static void clearCache() { - cache.clear(); + locationCache.clear(); + pathCache.clear(); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnection.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnection.java index 7daac7ee950f..1c0cda178d62 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnection.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnection.java @@ -38,7 +38,6 @@ import java.util.Map; import java.util.Map.Entry; -import org.springframework.boot.loader.net.util.UrlDecoder; import org.springframework.boot.loader.ref.Cleaner; /** @@ -77,7 +76,7 @@ class NestedUrlConnection extends URLConnection { private NestedLocation parseNestedLocation(URL url) throws MalformedURLException { try { - return NestedLocation.parse(UrlDecoder.decode(url.getPath())); + return NestedLocation.fromUrl(url); } catch (IllegalArgumentException ex) { throw new MalformedURLException(ex.getMessage()); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java index 40449813b837..7a5c8134c7eb 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java @@ -85,7 +85,7 @@ void fromUrlWhenNotNestedProtocolThrowsException() { @Test void fromUrlWhenNoPathThrowsException() { assertThatIllegalArgumentException().isThrownBy(() -> NestedLocation.fromUrl(new URL("nested:"))) - .withMessageContaining("'path' must not be empty"); + .withMessageContaining("'location' must not be empty"); } @Test