diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java index 401157b17d9..676eb046b1e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java @@ -154,7 +154,9 @@ public class NestedJarFile extends JarFile { @Override public Manifest getManifest() throws IOException { try { - return this.resources.zipContent().getInfo(ManifestInfo.class, this::getManifestInfo).getManifest(); + return this.resources.zipContentForManifest() + .getInfo(ManifestInfo.class, this::getManifestInfo) + .getManifest(); } catch (UncheckedIOException ex) { throw ex.getCause(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFileResources.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFileResources.java index 4f57e03497f..e1fb4d8d093 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFileResources.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFileResources.java @@ -30,6 +30,7 @@ import java.util.zip.Inflater; import org.springframework.boot.loader.ref.Cleaner; import org.springframework.boot.loader.zip.ZipContent; +import org.springframework.boot.loader.zip.ZipContent.Kind; /** * Resources created managed and cleaned by a {@link NestedJarFile} instance and suitable @@ -43,6 +44,8 @@ class NestedJarFileResources implements Runnable { private ZipContent zipContent; + private ZipContent zipContentForManifest; + private final Set inputStreams = Collections.newSetFromMap(new WeakHashMap<>()); private Deque inflaterCache = new ArrayDeque<>(); @@ -55,6 +58,8 @@ class NestedJarFileResources implements Runnable { */ NestedJarFileResources(File file, String nestedEntryName) throws IOException { this.zipContent = ZipContent.open(file.toPath(), nestedEntryName); + this.zipContentForManifest = (this.zipContent.getKind() != Kind.NESTED_DIRECTORY) ? null + : ZipContent.open(file.toPath()); } /** @@ -65,6 +70,15 @@ class NestedJarFileResources implements Runnable { return this.zipContent; } + /** + * Return the underlying {@link ZipContent} that should be used to load manifest + * content. + * @return the zip content to use when loading the manifest + */ + ZipContent zipContentForManifest() { + return (this.zipContentForManifest != null) ? this.zipContentForManifest : this.zipContent; + } + /** * Add a managed input stream resource. * @param inputStream the input stream @@ -144,6 +158,7 @@ class NestedJarFileResources implements Runnable { exceptionChain = releaseInflators(exceptionChain); exceptionChain = releaseInputStreams(exceptionChain); exceptionChain = releaseZipContent(exceptionChain); + exceptionChain = releaseZipContentForManifest(exceptionChain); if (exceptionChain != null) { throw new UncheckedIOException(exceptionChain); } @@ -195,6 +210,22 @@ class NestedJarFileResources implements Runnable { return exceptionChain; } + private IOException releaseZipContentForManifest(IOException exceptionChain) { + ZipContent zipContentForManifest = this.zipContentForManifest; + if (zipContentForManifest != null) { + try { + zipContentForManifest.close(); + } + catch (IOException ex) { + exceptionChain = addToExceptionChain(exceptionChain, ex); + } + finally { + this.zipContentForManifest = null; + } + } + return exceptionChain; + } + private IOException addToExceptionChain(IOException exceptionChain, IOException ex) { if (exceptionChain != null) { exceptionChain.addSuppressed(ex); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java index ca0f3d7a713..4e7298ffb4b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java @@ -72,6 +72,8 @@ public final class ZipContent implements Closeable { private final Source source; + private final Kind kind; + private final FileChannelDataBlock data; private final long centralDirectoryPos; @@ -94,10 +96,11 @@ public final class ZipContent implements Closeable { private SoftReference, Object>> info; - private ZipContent(Source source, FileChannelDataBlock data, long centralDirectoryPos, long commentPos, + private ZipContent(Source source, Kind kind, FileChannelDataBlock data, long centralDirectoryPos, long commentPos, long commentLength, int[] lookupIndexes, int[] nameHashLookups, int[] relativeCentralDirectoryOffsetLookups, NameOffsetLookups nameOffsetLookups, boolean hasJarSignatureFile) { this.source = source; + this.kind = kind; this.data = data; this.centralDirectoryPos = centralDirectoryPos; this.commentPos = commentPos; @@ -109,6 +112,15 @@ public final class ZipContent implements Closeable { this.hasJarSignatureFile = hasJarSignatureFile; } + /** + * Return the kind of content that was loaded. + * @return the content kind + * @since 3.2.2 + */ + public Kind getKind() { + return this.kind; + } + /** * Open a {@link DataBlock} containing the raw zip data. For container zip files, this * may be smaller than the original file since additional bytes are permitted at the @@ -380,6 +392,30 @@ public final class ZipContent implements Closeable { return zipContent; } + /** + * Zip content kinds. + * + * @since 3.2.2 + */ + public enum Kind { + + /** + * Content from a standard zip file. + */ + ZIP, + + /** + * Content from nested zip content. + */ + NESTED_ZIP, + + /** + * Content from a nested zip directory. + */ + NESTED_DIRECTORY + + } + /** * The source of {@link ZipContent}. Used as a cache key. * @@ -451,7 +487,7 @@ public final class ZipContent implements Closeable { this.cursor++; } - private ZipContent finish(long commentPos, long commentLength, boolean hasJarSignatureFile) { + private ZipContent finish(Kind kind, long commentPos, long commentLength, boolean hasJarSignatureFile) { if (this.cursor != this.nameHashLookups.length) { this.nameHashLookups = Arrays.copyOf(this.nameHashLookups, this.cursor); this.relativeCentralDirectoryOffsetLookups = Arrays.copyOf(this.relativeCentralDirectoryOffsetLookups, @@ -463,7 +499,7 @@ public final class ZipContent implements Closeable { for (int i = 0; i < size; i++) { lookupIndexes[this.index[i]] = i; } - return new ZipContent(this.source, this.data, this.centralDirectoryPos, commentPos, commentLength, + return new ZipContent(this.source, kind, this.data, this.centralDirectoryPos, commentPos, commentLength, lookupIndexes, this.nameHashLookups, this.relativeCentralDirectoryOffsetLookups, this.nameOffsetLookups, hasJarSignatureFile); } @@ -525,7 +561,7 @@ public final class ZipContent implements Closeable { private static ZipContent loadNonNested(Source source) throws IOException { debug.log("Loading non-nested zip '%s'", source.path()); - return openAndLoad(source, new FileChannelDataBlock(source.path())); + return openAndLoad(source, Kind.ZIP, new FileChannelDataBlock(source.path())); } private static ZipContent loadNestedZip(Source source, Entry entry) throws IOException { @@ -534,13 +570,13 @@ public final class ZipContent implements Closeable { .formatted(source.nestedEntryName(), source.path())); } debug.log("Loading nested zip entry '%s' from '%s'", source.nestedEntryName(), source.path()); - return openAndLoad(source, entry.getContent()); + return openAndLoad(source, Kind.NESTED_ZIP, entry.getContent()); } - private static ZipContent openAndLoad(Source source, FileChannelDataBlock data) throws IOException { + private static ZipContent openAndLoad(Source source, Kind kind, FileChannelDataBlock data) throws IOException { try { data.open(); - return loadContent(source, data); + return loadContent(source, kind, data); } catch (IOException | RuntimeException ex) { data.close(); @@ -548,7 +584,7 @@ public final class ZipContent implements Closeable { } } - private static ZipContent loadContent(Source source, FileChannelDataBlock data) throws IOException { + private static ZipContent loadContent(Source source, Kind kind, FileChannelDataBlock data) throws IOException { ZipEndOfCentralDirectoryRecord.Located locatedEocd = ZipEndOfCentralDirectoryRecord.load(data); ZipEndOfCentralDirectoryRecord eocd = locatedEocd.endOfCentralDirectoryRecord(); long eocdPos = locatedEocd.pos(); @@ -585,7 +621,7 @@ public final class ZipContent implements Closeable { pos += centralRecord.size(); } long commentPos = locatedEocd.pos() + ZipEndOfCentralDirectoryRecord.COMMENT_OFFSET; - return loader.finish(commentPos, eocd.commentLength(), hasJarSignatureFile); + return loader.finish(kind, commentPos, eocd.commentLength(), hasJarSignatureFile); } /** @@ -642,7 +678,7 @@ public final class ZipContent implements Closeable { } } } - return loader.finish(zip.commentPos, zip.commentLength, zip.hasJarSignatureFile); + return loader.finish(Kind.NESTED_DIRECTORY, zip.commentPos, zip.commentLength, zip.hasJarSignatureFile); } catch (IOException | RuntimeException ex) { zip.data.close(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java index 5c2aed4d5c4..ac6da2187b8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java @@ -110,6 +110,26 @@ class NestedJarFileTests { } } + @Test + void getManifestWhenNestedJarReturnsManifestOfNestedJar() throws Exception { + try (JarFile jar = new JarFile(this.file)) { + try (NestedJarFile nestedJar = new NestedJarFile(this.file, "nested.jar")) { + Manifest manifest = nestedJar.getManifest(); + assertThat(manifest).isNotEqualTo(jar.getManifest()); + assertThat(manifest.getMainAttributes().getValue("Built-By")).isEqualTo("j2"); + } + } + } + + @Test + void getManifestWhenNestedJarDirectoryReturnsManifestOfParent() throws Exception { + try (JarFile jar = new JarFile(this.file)) { + try (NestedJarFile nestedJar = new NestedJarFile(this.file, "d/")) { + assertThat(nestedJar.getManifest()).isEqualTo(jar.getManifest()); + } + } + } + @Test void createWhenJarHasFrontMatterOpensJar() throws IOException { File file = new File(this.tempDir, "frontmatter.jar"); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipContentTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipContentTests.java index 8681c430d6c..c2d8ff02e55 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipContentTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipContentTests.java @@ -48,6 +48,7 @@ import org.junit.jupiter.api.io.TempDir; import org.springframework.boot.loader.testsupport.TestJar; import org.springframework.boot.loader.zip.ZipContent.Entry; +import org.springframework.boot.loader.zip.ZipContent.Kind; import org.springframework.util.FileCopyUtils; import org.springframework.util.StreamUtils; @@ -168,6 +169,25 @@ class ZipContentTests { } } + @Test + void getKindWhenZipReturnsZip() { + assertThat(this.zipContent.getKind()).isEqualTo(Kind.ZIP); + } + + @Test + void getKindWhenNestedZipReturnsNestedZip() throws IOException { + try (ZipContent nested = ZipContent.open(this.file.toPath(), "nested.jar")) { + assertThat(nested.getKind()).isEqualTo(Kind.NESTED_ZIP); + } + } + + @Test + void getKindWhenNestedDirectoryReturnsNestedDirectory() throws IOException { + try (ZipContent nested = ZipContent.open(this.file.toPath(), "d/")) { + assertThat(nested.getKind()).isEqualTo(Kind.NESTED_DIRECTORY); + } + } + private void assertThatFieldsAreEqual(ZipEntry actual, ZipEntry expected) { assertThat(actual.getName()).isEqualTo(expected.getName()); assertThat(actual.getTime()).isEqualTo(expected.getTime());