From 9def6f86c9f88c405f302d9e044d5ea463cfe1f5 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Mon, 27 May 2024 12:01:57 +0200 Subject: [PATCH] Keep track of written jar entries to not duplicate them Closes gh-40903 --- .../boot/jarmode/tools/ExtractCommand.java | 16 ++++++++-- .../jarmode/tools/ExtractCommandTests.java | 30 +++++++++++++++++-- .../test/resources/jar-contents/empty-file | 0 3 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/empty-file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java index 3052f89ca397..200080138bd3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java @@ -29,6 +29,7 @@ import java.nio.file.attribute.FileTime; import java.util.EnumSet; import java.util.Enumeration; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -251,13 +252,22 @@ private void createApplication(JarStructure jarStructure, FileResolver fileResol mkdirs(file.getParentFile()); try (JarOutputStream output = new JarOutputStream(new FileOutputStream(file), manifest)) { EnumSet allowedTypes = EnumSet.of(Type.APPLICATION_CLASS_OR_RESOURCE, Type.META_INF); + Set writtenEntries = new HashSet<>(); withJarEntries(this.context.getArchiveFile(), ((stream, jarEntry) -> { Entry entry = jarStructure.resolve(jarEntry); if (entry != null && allowedTypes.contains(entry.type()) && StringUtils.hasLength(entry.location())) { JarEntry newJarEntry = createJarEntry(entry.location(), jarEntry); - output.putNextEntry(newJarEntry); - StreamUtils.copy(stream, output); - output.closeEntry(); + if (writtenEntries.add(newJarEntry.getName())) { + output.putNextEntry(newJarEntry); + StreamUtils.copy(stream, output); + output.closeEntry(); + } + else { + if (!newJarEntry.isDirectory()) { + throw new IllegalStateException("Duplicate jar entry '%s' from original location '%s'" + .formatted(newJarEntry.getName(), entry.originalLocation())); + } + } } })); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java index dbf90066ff39..0212d749c1ec 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java @@ -48,15 +48,17 @@ class ExtractCommandTests extends AbstractJarModeTests { private static final Instant LAST_ACCESS_TIME = Instant.parse("2022-01-01T00:00:00Z"); + private Manifest manifest; + private File archive; @BeforeEach void setUp() throws IOException { - Manifest manifest = createManifest("Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx", + this.manifest = createManifest("Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx", "Spring-Boot-Lib: BOOT-INF/lib/", "Spring-Boot-Classes: BOOT-INF/classes/", "Start-Class: org.example.Main", "Spring-Boot-Layers-Index: BOOT-INF/layers.idx", "Some-Attribute: Some-Value"); - this.archive = createArchive(manifest, CREATION_TIME, LAST_MODIFIED_TIME, LAST_ACCESS_TIME, + this.archive = createArchive(this.manifest, CREATION_TIME, LAST_MODIFIED_TIME, LAST_ACCESS_TIME, "BOOT-INF/classpath.idx", "/jar-contents/classpath.idx", "BOOT-INF/layers.idx", "/jar-contents/layers.idx", "BOOT-INF/lib/dependency-1.jar", "/jar-contents/dependency-1", "BOOT-INF/lib/dependency-2.jar", "/jar-contents/dependency-2", "BOOT-INF/lib/dependency-3-SNAPSHOT.jar", @@ -86,7 +88,7 @@ private void timeAttributes(File file) { catch (IOException ex) { throw new RuntimeException(ex); } - }; + } @Nested class Extract { @@ -216,6 +218,28 @@ void shouldExtractFilesUnderMetaInf() throws IOException { assertThat(entryNames).contains("META-INF/build-info.properties"); } + @Test + void shouldNotFailOnDuplicateDirectories() throws IOException { + File file = createArchive(ExtractCommandTests.this.manifest, "BOOT-INF/classpath.idx", + "/jar-contents/classpath.idx", "META-INF/native-image/", "/jar-contents/empty-file", + "BOOT-INF/classes/META-INF/native-image/", "/jar-contents/empty-file"); + run(file); + File application = file("test/test.jar"); + List entryNames = getJarEntryNames(application); + assertThat(entryNames).containsExactlyInAnyOrder("META-INF/native-image/", "META-INF/MANIFEST.MF"); + } + + @Test + void shouldFailOnDuplicateFiles() throws IOException { + File file = createArchive(ExtractCommandTests.this.manifest, "BOOT-INF/classpath.idx", + "/jar-contents/classpath.idx", "META-INF/native-image/native-image.properties", + "/jar-contents/empty-file", "BOOT-INF/classes/META-INF/native-image/native-image.properties", + "/jar-contents/empty-file"); + assertThatIllegalStateException().isThrownBy(() -> run(file)) + .withMessage( + "Duplicate jar entry 'META-INF/native-image/native-image.properties' from original location 'BOOT-INF/classes/META-INF/native-image/native-image.properties'"); + } + } @Nested diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/empty-file b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/empty-file new file mode 100644 index 000000000000..e69de29bb2d1