Skip to content

Commit

Permalink
Ensure META-INF/MANIFEST.MF remains as first entry
Browse files Browse the repository at this point in the history
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
  • Loading branch information
philwebb committed Jun 17, 2019
1 parent 5e3438f commit d82ccf1
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 132 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,19 @@
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;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
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;
Expand All @@ -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 {

Expand Down Expand Up @@ -88,192 +84,155 @@ class BootZipCopyAction implements CopyAction {

@Override
public WorkResult execute(CopyActionProcessingStream stream) {
ZipArchiveOutputStream zipStream;
Spec<FileTreeElement> 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<FileTreeElement> createExclusionSpec(Spec<FileTreeElement> loaderEntries) {
return Specs.union(loaderEntries, this.exclusions);
}

private Spec<FileTreeElement> writeLoaderClassesIfNecessary(ZipArchiveOutputStream out) {
if (!this.includeDefaultLoader) {
return Specs.satisfyNone();
finally {
closeQuietly(outputStream);
}
return writeLoaderClasses(out);
}

private Spec<FileTreeElement> writeLoaderClasses(ZipArchiveOutputStream out) {
try (ZipInputStream in = new ZipInputStream(
getClass().getResourceAsStream("/META-INF/loader/spring-boot-loader.jar"))) {
Set<String> 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<String, String> 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<FileTreeElement> requiresUnpack;
/**
* Internal process used to copy {@link FileCopyDetails file details} to the zip file.
*/
private class Processor {

private final Spec<FileTreeElement> exclusions;
private ZipArchiveOutputStream outputStream;

private final Function<FileCopyDetails, ZipCompression> compressionType;
private Spec<FileTreeElement> writtenLoaderEntries;

private ZipStreamAction(ZipArchiveOutputStream zipStream, File output, boolean preserveFileTimestamps,
Spec<FileTreeElement> requiresUnpack, Spec<FileTreeElement> exclusions,
Function<FileCopyDetails, ZipCompression> 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;
}

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

}
Expand Down
Loading

0 comments on commit d82ccf1

Please sign in to comment.