From d82ccf14059e2dcf25334018a00833fa8cbd29ab Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 17 Jun 2019 15:41:00 -0700 Subject: [PATCH] Ensure META-INF/MANIFEST.MF remains as first entry Update Gradle archive tasks to ensure that `META-INF/` and `META-INF/MANIFEST.MF` remain as the first entries of the archive. Prior to this commit, rewritten archives would violate the implicit specification of `JarInputStream` that these entries should be first. Fixes gh-16698 --- .../tasks/bundling/BootZipCopyAction.java | 221 +++++++----------- .../tasks/bundling/LoaderZipEntries.java | 116 +++++++++ .../bundling/AbstractBootArchiveTests.java | 9 +- 3 files changed, 214 insertions(+), 132 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java index c8532f87a369..7ec06a91043c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java @@ -22,11 +22,9 @@ import java.io.OutputStream; import java.util.Calendar; import java.util.GregorianCalendar; -import java.util.HashSet; -import java.util.Set; +import java.util.Map; import java.util.function.Function; import java.util.zip.CRC32; -import java.util.zip.ZipInputStream; import org.apache.commons.compress.archivers.zip.UnixStat; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; @@ -34,12 +32,9 @@ import org.gradle.api.GradleException; import org.gradle.api.file.FileCopyDetails; import org.gradle.api.file.FileTreeElement; -import org.gradle.api.internal.file.CopyActionProcessingStreamAction; import org.gradle.api.internal.file.copy.CopyAction; import org.gradle.api.internal.file.copy.CopyActionProcessingStream; -import org.gradle.api.internal.file.copy.FileCopyDetailsInternal; import org.gradle.api.specs.Spec; -import org.gradle.api.specs.Specs; import org.gradle.api.tasks.WorkResult; import org.springframework.boot.loader.tools.DefaultLaunchScript; @@ -50,6 +45,7 @@ * Stores jar files without compression as required by Spring Boot's loader. * * @author Andy Wilkinson + * @author Phillip Webb */ class BootZipCopyAction implements CopyAction { @@ -88,192 +84,155 @@ class BootZipCopyAction implements CopyAction { @Override public WorkResult execute(CopyActionProcessingStream stream) { - ZipArchiveOutputStream zipStream; - Spec loaderEntries; try { - FileOutputStream fileStream = new FileOutputStream(this.output); - writeLaunchScriptIfNecessary(fileStream); - zipStream = new ZipArchiveOutputStream(fileStream); - if (this.encoding != null) { - zipStream.setEncoding(this.encoding); - } - loaderEntries = writeLoaderClassesIfNecessary(zipStream); + writeArchive(stream); + return () -> true; } catch (IOException ex) { throw new GradleException("Failed to create " + this.output, ex); } + } + + private void writeArchive(CopyActionProcessingStream stream) throws IOException { + OutputStream outputStream = new FileOutputStream(this.output); try { - stream.process(new ZipStreamAction(zipStream, this.output, this.preserveFileTimestamps, this.requiresUnpack, - createExclusionSpec(loaderEntries), this.compressionResolver)); - } - finally { + writeLaunchScriptIfNecessary(outputStream); + ZipArchiveOutputStream zipOutputStream = new ZipArchiveOutputStream(outputStream); try { - zipStream.close(); + if (this.encoding != null) { + zipOutputStream.setEncoding(this.encoding); + } + Processor processor = new Processor(zipOutputStream); + stream.process(processor::process); + processor.finish(); } - catch (IOException ex) { - // Continue + finally { + closeQuietly(zipOutputStream); } } - return () -> true; - } - - @SuppressWarnings("unchecked") - private Spec createExclusionSpec(Spec loaderEntries) { - return Specs.union(loaderEntries, this.exclusions); - } - - private Spec writeLoaderClassesIfNecessary(ZipArchiveOutputStream out) { - if (!this.includeDefaultLoader) { - return Specs.satisfyNone(); + finally { + closeQuietly(outputStream); } - return writeLoaderClasses(out); } - private Spec writeLoaderClasses(ZipArchiveOutputStream out) { - try (ZipInputStream in = new ZipInputStream( - getClass().getResourceAsStream("/META-INF/loader/spring-boot-loader.jar"))) { - Set entries = new HashSet<>(); - java.util.zip.ZipEntry entry; - while ((entry = in.getNextEntry()) != null) { - if (entry.isDirectory() && !entry.getName().startsWith("META-INF/")) { - writeDirectory(new ZipArchiveEntry(entry), out); - entries.add(entry.getName()); - } - else if (entry.getName().endsWith(".class")) { - writeClass(new ZipArchiveEntry(entry), in, out); - } - } - return (element) -> { - String path = element.getRelativePath().getPathString(); - if (element.isDirectory() && !path.endsWith(("/"))) { - path += "/"; - } - return entries.contains(path); - }; - } - catch (IOException ex) { - throw new GradleException("Failed to write loader classes", ex); + private void writeLaunchScriptIfNecessary(OutputStream outputStream) { + if (this.launchScript == null) { + return; } - } - - private void writeDirectory(ZipArchiveEntry entry, ZipArchiveOutputStream out) throws IOException { - prepareEntry(entry, UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM); - out.putArchiveEntry(entry); - out.closeArchiveEntry(); - } - - private void writeClass(ZipArchiveEntry entry, ZipInputStream in, ZipArchiveOutputStream out) throws IOException { - prepareEntry(entry, UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM); - out.putArchiveEntry(entry); - byte[] buffer = new byte[4096]; - int read; - while ((read = in.read(buffer)) > 0) { - out.write(buffer, 0, read); + try { + File file = this.launchScript.getScript(); + Map properties = this.launchScript.getProperties(); + outputStream.write(new DefaultLaunchScript(file, properties).toByteArray()); + outputStream.flush(); + this.output.setExecutable(true); } - out.closeArchiveEntry(); - } - - private void prepareEntry(ZipArchiveEntry entry, int unixMode) { - if (!this.preserveFileTimestamps) { - entry.setTime(CONSTANT_TIME_FOR_ZIP_ENTRIES); + catch (IOException ex) { + throw new GradleException("Failed to write launch script to " + this.output, ex); } - entry.setUnixMode(unixMode); } - private void writeLaunchScriptIfNecessary(FileOutputStream fileStream) { + private void closeQuietly(OutputStream outputStream) { try { - if (this.launchScript != null) { - fileStream - .write(new DefaultLaunchScript(this.launchScript.getScript(), this.launchScript.getProperties()) - .toByteArray()); - this.output.setExecutable(true); - } + outputStream.close(); } catch (IOException ex) { - throw new GradleException("Failed to write launch script to " + this.output, ex); } } - private static final class ZipStreamAction implements CopyActionProcessingStreamAction { - - private final ZipArchiveOutputStream zipStream; - - private final File output; - - private final boolean preserveFileTimestamps; - - private final Spec requiresUnpack; + /** + * Internal process used to copy {@link FileCopyDetails file details} to the zip file. + */ + private class Processor { - private final Spec exclusions; + private ZipArchiveOutputStream outputStream; - private final Function compressionType; + private Spec writtenLoaderEntries; - private ZipStreamAction(ZipArchiveOutputStream zipStream, File output, boolean preserveFileTimestamps, - Spec requiresUnpack, Spec exclusions, - Function compressionType) { - this.zipStream = zipStream; - this.output = output; - this.preserveFileTimestamps = preserveFileTimestamps; - this.requiresUnpack = requiresUnpack; - this.exclusions = exclusions; - this.compressionType = compressionType; + Processor(ZipArchiveOutputStream outputStream) { + this.outputStream = outputStream; } - @Override - public void processFile(FileCopyDetailsInternal details) { - if (this.exclusions.isSatisfiedBy(details)) { + public void process(FileCopyDetails details) { + if (BootZipCopyAction.this.exclusions.isSatisfiedBy(details) + || (this.writtenLoaderEntries != null && this.writtenLoaderEntries.isSatisfiedBy(details))) { return; } try { + writeLoaderEntriesIfNecessary(details); if (details.isDirectory()) { - createDirectory(details); + processDirectory(details); } else { - createFile(details); + processFile(details); } } catch (IOException ex) { - throw new GradleException("Failed to add " + details + " to " + this.output, ex); + throw new GradleException("Failed to add " + details + " to " + BootZipCopyAction.this.output, ex); + } + } + + public void finish() throws IOException { + writeLoaderEntriesIfNecessary(null); + } + + private void writeLoaderEntriesIfNecessary(FileCopyDetails details) throws IOException { + if (!BootZipCopyAction.this.includeDefaultLoader || this.writtenLoaderEntries != null) { + return; + } + if (isInMetaInf(details)) { + // Don't write loader entries until after META-INF folder (see gh-16698) + return; + } + LoaderZipEntries loaderEntries = new LoaderZipEntries( + BootZipCopyAction.this.preserveFileTimestamps ? null : CONSTANT_TIME_FOR_ZIP_ENTRIES); + this.writtenLoaderEntries = loaderEntries.writeTo(this.outputStream); + } + + private boolean isInMetaInf(FileCopyDetails details) { + if (details == null) { + return false; } + String[] segments = details.getRelativePath().getSegments(); + return segments.length > 0 && "META-INF".equals(segments[0]); } - private void createDirectory(FileCopyDetailsInternal details) throws IOException { + private void processDirectory(FileCopyDetails details) throws IOException { ZipArchiveEntry archiveEntry = new ZipArchiveEntry(details.getRelativePath().getPathString() + '/'); archiveEntry.setUnixMode(UnixStat.DIR_FLAG | details.getMode()); archiveEntry.setTime(getTime(details)); - this.zipStream.putArchiveEntry(archiveEntry); - this.zipStream.closeArchiveEntry(); + this.outputStream.putArchiveEntry(archiveEntry); + this.outputStream.closeArchiveEntry(); } - private void createFile(FileCopyDetailsInternal details) throws IOException { + private void processFile(FileCopyDetails details) throws IOException { String relativePath = details.getRelativePath().getPathString(); ZipArchiveEntry archiveEntry = new ZipArchiveEntry(relativePath); archiveEntry.setUnixMode(UnixStat.FILE_FLAG | details.getMode()); archiveEntry.setTime(getTime(details)); - ZipCompression compression = this.compressionType.apply(details); + ZipCompression compression = BootZipCopyAction.this.compressionResolver.apply(details); if (compression == ZipCompression.STORED) { prepareStoredEntry(details, archiveEntry); } - this.zipStream.putArchiveEntry(archiveEntry); - details.copyTo(this.zipStream); - this.zipStream.closeArchiveEntry(); + this.outputStream.putArchiveEntry(archiveEntry); + details.copyTo(this.outputStream); + this.outputStream.closeArchiveEntry(); } - private void prepareStoredEntry(FileCopyDetailsInternal details, ZipArchiveEntry archiveEntry) - throws IOException { + private void prepareStoredEntry(FileCopyDetails details, ZipArchiveEntry archiveEntry) throws IOException { archiveEntry.setMethod(java.util.zip.ZipEntry.STORED); archiveEntry.setSize(details.getSize()); archiveEntry.setCompressedSize(details.getSize()); Crc32OutputStream crcStream = new Crc32OutputStream(); details.copyTo(crcStream); archiveEntry.setCrc(crcStream.getCrc()); - if (this.requiresUnpack.isSatisfiedBy(details)) { + if (BootZipCopyAction.this.requiresUnpack.isSatisfiedBy(details)) { archiveEntry.setComment("UNPACK:" + FileUtils.sha1Hash(details.getFile())); } } private long getTime(FileCopyDetails details) { - return this.preserveFileTimestamps ? details.getLastModified() : CONSTANT_TIME_FOR_ZIP_ENTRIES; + return BootZipCopyAction.this.preserveFileTimestamps ? details.getLastModified() + : CONSTANT_TIME_FOR_ZIP_ENTRIES; } } @@ -283,25 +242,25 @@ private long getTime(FileCopyDetails details) { */ private static final class Crc32OutputStream extends OutputStream { - private final CRC32 crc32 = new CRC32(); + private final CRC32 crc = new CRC32(); @Override public void write(int b) throws IOException { - this.crc32.update(b); + this.crc.update(b); } @Override public void write(byte[] b) throws IOException { - this.crc32.update(b); + this.crc.update(b); } @Override public void write(byte[] b, int off, int len) throws IOException { - this.crc32.update(b, off, len); + this.crc.update(b, off, len); } private long getCrc() { - return this.crc32.getValue(); + return this.crc.getValue(); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java new file mode 100644 index 000000000000..818ae0358f97 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java @@ -0,0 +1,116 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.gradle.tasks.bundling; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashSet; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import org.apache.commons.compress.archivers.zip.UnixStat; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; +import org.gradle.api.file.FileTreeElement; +import org.gradle.api.specs.Spec; + +/** + * Internal utility used to copy entries from the {@code spring-boot-loader.jar}. + * + * @author Andy Wilkinson + * @author Phillip Webb + */ +class LoaderZipEntries { + + private Long entryTime; + + LoaderZipEntries(Long entryTime) { + this.entryTime = entryTime; + } + + public Spec writeTo(ZipArchiveOutputStream zipOutputStream) throws IOException { + WrittenDirectoriesSpec writtenDirectoriesSpec = new WrittenDirectoriesSpec(); + try (ZipInputStream loaderJar = new ZipInputStream( + getClass().getResourceAsStream("/META-INF/loader/spring-boot-loader.jar"))) { + java.util.zip.ZipEntry entry = loaderJar.getNextEntry(); + while (entry != null) { + if (entry.isDirectory() && !entry.getName().equals("META-INF/")) { + writeDirectory(new ZipArchiveEntry(entry), zipOutputStream); + writtenDirectoriesSpec.add(entry); + } + else if (entry.getName().endsWith(".class")) { + writeClass(new ZipArchiveEntry(entry), loaderJar, zipOutputStream); + } + entry = loaderJar.getNextEntry(); + } + } + return writtenDirectoriesSpec; + } + + private void writeDirectory(ZipArchiveEntry entry, ZipArchiveOutputStream out) throws IOException { + prepareEntry(entry, UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM); + out.putArchiveEntry(entry); + out.closeArchiveEntry(); + } + + private void writeClass(ZipArchiveEntry entry, ZipInputStream in, ZipArchiveOutputStream out) throws IOException { + prepareEntry(entry, UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM); + out.putArchiveEntry(entry); + copy(in, out); + out.closeArchiveEntry(); + } + + private void prepareEntry(ZipArchiveEntry entry, int unixMode) { + if (this.entryTime != null) { + entry.setTime(this.entryTime); + } + entry.setUnixMode(unixMode); + } + + private void copy(InputStream in, OutputStream out) throws IOException { + byte[] buffer = new byte[4096]; + int bytesRead = -1; + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + } + } + + /** + * Spec to track directories that have been written. + */ + private static class WrittenDirectoriesSpec implements Spec { + + private final Set entries = new HashSet<>(); + + @Override + public boolean isSatisfiedBy(FileTreeElement element) { + String path = element.getRelativePath().getPathString(); + if (element.isDirectory() && !path.endsWith(("/"))) { + path += "/"; + } + return this.entries.contains(path); + } + + public void add(ZipEntry entry) { + this.entries.add(entry.getName()); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java index 3d535793bd76..9cf58f9c6518 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.gradle.tasks.bundling; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; @@ -34,6 +35,7 @@ import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipFile; @@ -187,13 +189,18 @@ public void filesOnTheClasspathThatAreNotZipFilesAreSkipped() throws IOException } @Test - public void loaderIsWrittenToTheRootOfTheJar() throws IOException { + public void loaderIsWrittenToTheRootOfTheJarAfterManifest() throws IOException { this.task.setMainClassName("com.example.Main"); this.task.execute(); try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { assertThat(jarFile.getEntry("org/springframework/boot/loader/LaunchedURLClassLoader.class")).isNotNull(); assertThat(jarFile.getEntry("org/springframework/boot/loader/")).isNotNull(); } + // gh-16698 + try (ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(this.task.getArchivePath()))) { + assertThat(zipInputStream.getNextEntry().getName()).isEqualTo("META-INF/"); + assertThat(zipInputStream.getNextEntry().getName()).isEqualTo("META-INF/MANIFEST.MF"); + } } @Test