From e5f489f338a185f9a6b34d8f320774c6810bb7ff Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 9 Jan 2024 12:33:10 -0800 Subject: [PATCH] Restore manifest support for nested directory jars Update `NestedJarFile` so that the `getManifest()` method returns the manifest from the parent jar file for nested jars based on directory entries. This restores the previous behavior supported by Spring Boot 3.1 and allows class methods such as `getPackage().getImplementationVersion()` to return non `null` results. Fixes gh-38996 --- .../boot/loader/jar/NestedJarFile.java | 4 +- .../loader/jar/NestedJarFileResources.java | 31 ++++++++++ .../boot/loader/zip/ZipContent.java | 56 +++++++++++++++---- .../boot/loader/jar/NestedJarFileTests.java | 20 +++++++ .../boot/loader/zip/ZipContentTests.java | 20 +++++++ 5 files changed, 120 insertions(+), 11 deletions(-) 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());