diff --git a/loader/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java b/loader/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java index 41fc37b57a0..988c52af554 100644 --- a/loader/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java +++ b/loader/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java @@ -26,6 +26,7 @@ import java.io.OutputStream; import java.io.PrintStream; import java.io.UncheckedIOException; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; @@ -57,6 +58,7 @@ import org.springframework.util.StringUtils; * The {@code 'extract'} tools command. * * @author Moritz Halbritter + * @author Dongliang Xie */ class ExtractCommand extends Command { @@ -363,14 +365,18 @@ class ExtractCommand extends Command { } private static File assertFileIsContainedInDirectory(File directory, File file, String name) throws IOException { - String canonicalOutputPath = directory.getCanonicalPath() + File.separator; - String canonicalEntryPath = file.getCanonicalPath(); - Assert.state(canonicalEntryPath.startsWith(canonicalOutputPath), + Path canonicalOutputPath = directory.getCanonicalFile().toPath(); + Path canonicalEntryPath = file.getCanonicalFile().toPath(); + Assert.state(isFileContainedInDirectory(canonicalOutputPath, canonicalEntryPath), () -> "Entry '%s' would be written to '%s'. This is outside the output location of '%s'. Verify the contents of your archive." .formatted(name, canonicalEntryPath, canonicalOutputPath)); return file; } + private static boolean isFileContainedInDirectory(Path directory, Path file) { + return !file.equals(directory) && file.startsWith(directory); + } + @FunctionalInterface private interface EntryNameTransformer { @@ -515,9 +521,9 @@ class ExtractCommand extends Command { } private File assertLayerDirectoryLocation(File layerDirectory, String layerName) throws IOException { - String canonicalOutputPath = this.directory.getCanonicalPath() + File.separator; - String canonicalLayerPath = layerDirectory.getCanonicalPath(); - Assert.state(canonicalLayerPath.startsWith(canonicalOutputPath), + Path canonicalOutputPath = this.directory.getCanonicalFile().toPath(); + Path canonicalLayerPath = layerDirectory.getCanonicalFile().toPath(); + Assert.state(isFileContainedInDirectory(canonicalOutputPath, canonicalLayerPath), () -> "Layer '%s' would be written to '%s'. This is outside the output location of '%s'. Verify the contents of your archive." .formatted(layerName, canonicalLayerPath, canonicalOutputPath)); return layerDirectory; diff --git a/loader/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java b/loader/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java index 2f25c094d1a..911318f9e69 100644 --- a/loader/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java +++ b/loader/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java @@ -20,11 +20,13 @@ import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.BasicFileAttributes; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Enumeration; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.jar.Manifest; @@ -45,6 +47,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; * Tests for {@link ExtractCommand}. * * @author Moritz Halbritter + * @author Dongliang Xie */ class ExtractCommandTests extends AbstractJarModeTests { @@ -370,6 +373,38 @@ class ExtractCommandTests extends AbstractJarModeTests { .doesNotContain("test/spring-boot-loader/org/springframework/boot/loader/launch/JarLauncher.class"); } + @Test + void extractWhenDestinationIsFileSystemRoot() throws IOException { + Path layerDirectory = ExtractCommandTests.this.tempDir.toPath() + .resolve("root-output") + .resolve("dependencies") + .toAbsolutePath() + .normalize(); + Path outputRoot = layerDirectory.getRoot(); + String layerName = outputRoot.relativize(layerDirectory).toString().replace(File.separatorChar, '/'); + Layers layers = new Layers() { + + @Override + public Iterator iterator() { + return List.of(layerName).iterator(); + } + + @Override + public String getLayer(String entryName) { + return layerName; + } + + @Override + public String getApplicationLayerName() { + return layerName; + } + + }; + runCommand((context) -> new ExtractCommand(context, layers), ExtractCommandTests.this.archive, + "--destination", outputRoot.toString(), "--force", "--launcher", "--layers"); + assertThat(layerDirectory.resolve("BOOT-INF/lib/dependency-1.jar")).exists(); + } + } }