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
This commit is contained in:
Phillip Webb 2024-01-09 12:33:10 -08:00
parent 36f00fcc33
commit e5f489f338
5 changed files with 120 additions and 11 deletions

View File

@ -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();

View File

@ -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<InputStream> inputStreams = Collections.newSetFromMap(new WeakHashMap<>());
private Deque<Inflater> 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);

View File

@ -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<Map<Class<?>, 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();

View File

@ -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");

View File

@ -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());