Skip to content

Commit d82ccf1

Browse files
committed
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
1 parent 5e3438f commit d82ccf1

File tree

3 files changed

+214
-132
lines changed

3 files changed

+214
-132
lines changed

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java

Lines changed: 90 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,19 @@
2222
import java.io.OutputStream;
2323
import java.util.Calendar;
2424
import java.util.GregorianCalendar;
25-
import java.util.HashSet;
26-
import java.util.Set;
25+
import java.util.Map;
2726
import java.util.function.Function;
2827
import java.util.zip.CRC32;
29-
import java.util.zip.ZipInputStream;
3028

3129
import org.apache.commons.compress.archivers.zip.UnixStat;
3230
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
3331
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
3432
import org.gradle.api.GradleException;
3533
import org.gradle.api.file.FileCopyDetails;
3634
import org.gradle.api.file.FileTreeElement;
37-
import org.gradle.api.internal.file.CopyActionProcessingStreamAction;
3835
import org.gradle.api.internal.file.copy.CopyAction;
3936
import org.gradle.api.internal.file.copy.CopyActionProcessingStream;
40-
import org.gradle.api.internal.file.copy.FileCopyDetailsInternal;
4137
import org.gradle.api.specs.Spec;
42-
import org.gradle.api.specs.Specs;
4338
import org.gradle.api.tasks.WorkResult;
4439

4540
import org.springframework.boot.loader.tools.DefaultLaunchScript;
@@ -50,6 +45,7 @@
5045
* Stores jar files without compression as required by Spring Boot's loader.
5146
*
5247
* @author Andy Wilkinson
48+
* @author Phillip Webb
5349
*/
5450
class BootZipCopyAction implements CopyAction {
5551

@@ -88,192 +84,155 @@ class BootZipCopyAction implements CopyAction {
8884

8985
@Override
9086
public WorkResult execute(CopyActionProcessingStream stream) {
91-
ZipArchiveOutputStream zipStream;
92-
Spec<FileTreeElement> loaderEntries;
9387
try {
94-
FileOutputStream fileStream = new FileOutputStream(this.output);
95-
writeLaunchScriptIfNecessary(fileStream);
96-
zipStream = new ZipArchiveOutputStream(fileStream);
97-
if (this.encoding != null) {
98-
zipStream.setEncoding(this.encoding);
99-
}
100-
loaderEntries = writeLoaderClassesIfNecessary(zipStream);
88+
writeArchive(stream);
89+
return () -> true;
10190
}
10291
catch (IOException ex) {
10392
throw new GradleException("Failed to create " + this.output, ex);
10493
}
94+
}
95+
96+
private void writeArchive(CopyActionProcessingStream stream) throws IOException {
97+
OutputStream outputStream = new FileOutputStream(this.output);
10598
try {
106-
stream.process(new ZipStreamAction(zipStream, this.output, this.preserveFileTimestamps, this.requiresUnpack,
107-
createExclusionSpec(loaderEntries), this.compressionResolver));
108-
}
109-
finally {
99+
writeLaunchScriptIfNecessary(outputStream);
100+
ZipArchiveOutputStream zipOutputStream = new ZipArchiveOutputStream(outputStream);
110101
try {
111-
zipStream.close();
102+
if (this.encoding != null) {
103+
zipOutputStream.setEncoding(this.encoding);
104+
}
105+
Processor processor = new Processor(zipOutputStream);
106+
stream.process(processor::process);
107+
processor.finish();
112108
}
113-
catch (IOException ex) {
114-
// Continue
109+
finally {
110+
closeQuietly(zipOutputStream);
115111
}
116112
}
117-
return () -> true;
118-
}
119-
120-
@SuppressWarnings("unchecked")
121-
private Spec<FileTreeElement> createExclusionSpec(Spec<FileTreeElement> loaderEntries) {
122-
return Specs.union(loaderEntries, this.exclusions);
123-
}
124-
125-
private Spec<FileTreeElement> writeLoaderClassesIfNecessary(ZipArchiveOutputStream out) {
126-
if (!this.includeDefaultLoader) {
127-
return Specs.satisfyNone();
113+
finally {
114+
closeQuietly(outputStream);
128115
}
129-
return writeLoaderClasses(out);
130116
}
131117

132-
private Spec<FileTreeElement> writeLoaderClasses(ZipArchiveOutputStream out) {
133-
try (ZipInputStream in = new ZipInputStream(
134-
getClass().getResourceAsStream("/META-INF/loader/spring-boot-loader.jar"))) {
135-
Set<String> entries = new HashSet<>();
136-
java.util.zip.ZipEntry entry;
137-
while ((entry = in.getNextEntry()) != null) {
138-
if (entry.isDirectory() && !entry.getName().startsWith("META-INF/")) {
139-
writeDirectory(new ZipArchiveEntry(entry), out);
140-
entries.add(entry.getName());
141-
}
142-
else if (entry.getName().endsWith(".class")) {
143-
writeClass(new ZipArchiveEntry(entry), in, out);
144-
}
145-
}
146-
return (element) -> {
147-
String path = element.getRelativePath().getPathString();
148-
if (element.isDirectory() && !path.endsWith(("/"))) {
149-
path += "/";
150-
}
151-
return entries.contains(path);
152-
};
153-
}
154-
catch (IOException ex) {
155-
throw new GradleException("Failed to write loader classes", ex);
118+
private void writeLaunchScriptIfNecessary(OutputStream outputStream) {
119+
if (this.launchScript == null) {
120+
return;
156121
}
157-
}
158-
159-
private void writeDirectory(ZipArchiveEntry entry, ZipArchiveOutputStream out) throws IOException {
160-
prepareEntry(entry, UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM);
161-
out.putArchiveEntry(entry);
162-
out.closeArchiveEntry();
163-
}
164-
165-
private void writeClass(ZipArchiveEntry entry, ZipInputStream in, ZipArchiveOutputStream out) throws IOException {
166-
prepareEntry(entry, UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM);
167-
out.putArchiveEntry(entry);
168-
byte[] buffer = new byte[4096];
169-
int read;
170-
while ((read = in.read(buffer)) > 0) {
171-
out.write(buffer, 0, read);
122+
try {
123+
File file = this.launchScript.getScript();
124+
Map<String, String> properties = this.launchScript.getProperties();
125+
outputStream.write(new DefaultLaunchScript(file, properties).toByteArray());
126+
outputStream.flush();
127+
this.output.setExecutable(true);
172128
}
173-
out.closeArchiveEntry();
174-
}
175-
176-
private void prepareEntry(ZipArchiveEntry entry, int unixMode) {
177-
if (!this.preserveFileTimestamps) {
178-
entry.setTime(CONSTANT_TIME_FOR_ZIP_ENTRIES);
129+
catch (IOException ex) {
130+
throw new GradleException("Failed to write launch script to " + this.output, ex);
179131
}
180-
entry.setUnixMode(unixMode);
181132
}
182133

183-
private void writeLaunchScriptIfNecessary(FileOutputStream fileStream) {
134+
private void closeQuietly(OutputStream outputStream) {
184135
try {
185-
if (this.launchScript != null) {
186-
fileStream
187-
.write(new DefaultLaunchScript(this.launchScript.getScript(), this.launchScript.getProperties())
188-
.toByteArray());
189-
this.output.setExecutable(true);
190-
}
136+
outputStream.close();
191137
}
192138
catch (IOException ex) {
193-
throw new GradleException("Failed to write launch script to " + this.output, ex);
194139
}
195140
}
196141

197-
private static final class ZipStreamAction implements CopyActionProcessingStreamAction {
198-
199-
private final ZipArchiveOutputStream zipStream;
200-
201-
private final File output;
202-
203-
private final boolean preserveFileTimestamps;
204-
205-
private final Spec<FileTreeElement> requiresUnpack;
142+
/**
143+
* Internal process used to copy {@link FileCopyDetails file details} to the zip file.
144+
*/
145+
private class Processor {
206146

207-
private final Spec<FileTreeElement> exclusions;
147+
private ZipArchiveOutputStream outputStream;
208148

209-
private final Function<FileCopyDetails, ZipCompression> compressionType;
149+
private Spec<FileTreeElement> writtenLoaderEntries;
210150

211-
private ZipStreamAction(ZipArchiveOutputStream zipStream, File output, boolean preserveFileTimestamps,
212-
Spec<FileTreeElement> requiresUnpack, Spec<FileTreeElement> exclusions,
213-
Function<FileCopyDetails, ZipCompression> compressionType) {
214-
this.zipStream = zipStream;
215-
this.output = output;
216-
this.preserveFileTimestamps = preserveFileTimestamps;
217-
this.requiresUnpack = requiresUnpack;
218-
this.exclusions = exclusions;
219-
this.compressionType = compressionType;
151+
Processor(ZipArchiveOutputStream outputStream) {
152+
this.outputStream = outputStream;
220153
}
221154

222-
@Override
223-
public void processFile(FileCopyDetailsInternal details) {
224-
if (this.exclusions.isSatisfiedBy(details)) {
155+
public void process(FileCopyDetails details) {
156+
if (BootZipCopyAction.this.exclusions.isSatisfiedBy(details)
157+
|| (this.writtenLoaderEntries != null && this.writtenLoaderEntries.isSatisfiedBy(details))) {
225158
return;
226159
}
227160
try {
161+
writeLoaderEntriesIfNecessary(details);
228162
if (details.isDirectory()) {
229-
createDirectory(details);
163+
processDirectory(details);
230164
}
231165
else {
232-
createFile(details);
166+
processFile(details);
233167
}
234168
}
235169
catch (IOException ex) {
236-
throw new GradleException("Failed to add " + details + " to " + this.output, ex);
170+
throw new GradleException("Failed to add " + details + " to " + BootZipCopyAction.this.output, ex);
171+
}
172+
}
173+
174+
public void finish() throws IOException {
175+
writeLoaderEntriesIfNecessary(null);
176+
}
177+
178+
private void writeLoaderEntriesIfNecessary(FileCopyDetails details) throws IOException {
179+
if (!BootZipCopyAction.this.includeDefaultLoader || this.writtenLoaderEntries != null) {
180+
return;
181+
}
182+
if (isInMetaInf(details)) {
183+
// Don't write loader entries until after META-INF folder (see gh-16698)
184+
return;
185+
}
186+
LoaderZipEntries loaderEntries = new LoaderZipEntries(
187+
BootZipCopyAction.this.preserveFileTimestamps ? null : CONSTANT_TIME_FOR_ZIP_ENTRIES);
188+
this.writtenLoaderEntries = loaderEntries.writeTo(this.outputStream);
189+
}
190+
191+
private boolean isInMetaInf(FileCopyDetails details) {
192+
if (details == null) {
193+
return false;
237194
}
195+
String[] segments = details.getRelativePath().getSegments();
196+
return segments.length > 0 && "META-INF".equals(segments[0]);
238197
}
239198

240-
private void createDirectory(FileCopyDetailsInternal details) throws IOException {
199+
private void processDirectory(FileCopyDetails details) throws IOException {
241200
ZipArchiveEntry archiveEntry = new ZipArchiveEntry(details.getRelativePath().getPathString() + '/');
242201
archiveEntry.setUnixMode(UnixStat.DIR_FLAG | details.getMode());
243202
archiveEntry.setTime(getTime(details));
244-
this.zipStream.putArchiveEntry(archiveEntry);
245-
this.zipStream.closeArchiveEntry();
203+
this.outputStream.putArchiveEntry(archiveEntry);
204+
this.outputStream.closeArchiveEntry();
246205
}
247206

248-
private void createFile(FileCopyDetailsInternal details) throws IOException {
207+
private void processFile(FileCopyDetails details) throws IOException {
249208
String relativePath = details.getRelativePath().getPathString();
250209
ZipArchiveEntry archiveEntry = new ZipArchiveEntry(relativePath);
251210
archiveEntry.setUnixMode(UnixStat.FILE_FLAG | details.getMode());
252211
archiveEntry.setTime(getTime(details));
253-
ZipCompression compression = this.compressionType.apply(details);
212+
ZipCompression compression = BootZipCopyAction.this.compressionResolver.apply(details);
254213
if (compression == ZipCompression.STORED) {
255214
prepareStoredEntry(details, archiveEntry);
256215
}
257-
this.zipStream.putArchiveEntry(archiveEntry);
258-
details.copyTo(this.zipStream);
259-
this.zipStream.closeArchiveEntry();
216+
this.outputStream.putArchiveEntry(archiveEntry);
217+
details.copyTo(this.outputStream);
218+
this.outputStream.closeArchiveEntry();
260219
}
261220

262-
private void prepareStoredEntry(FileCopyDetailsInternal details, ZipArchiveEntry archiveEntry)
263-
throws IOException {
221+
private void prepareStoredEntry(FileCopyDetails details, ZipArchiveEntry archiveEntry) throws IOException {
264222
archiveEntry.setMethod(java.util.zip.ZipEntry.STORED);
265223
archiveEntry.setSize(details.getSize());
266224
archiveEntry.setCompressedSize(details.getSize());
267225
Crc32OutputStream crcStream = new Crc32OutputStream();
268226
details.copyTo(crcStream);
269227
archiveEntry.setCrc(crcStream.getCrc());
270-
if (this.requiresUnpack.isSatisfiedBy(details)) {
228+
if (BootZipCopyAction.this.requiresUnpack.isSatisfiedBy(details)) {
271229
archiveEntry.setComment("UNPACK:" + FileUtils.sha1Hash(details.getFile()));
272230
}
273231
}
274232

275233
private long getTime(FileCopyDetails details) {
276-
return this.preserveFileTimestamps ? details.getLastModified() : CONSTANT_TIME_FOR_ZIP_ENTRIES;
234+
return BootZipCopyAction.this.preserveFileTimestamps ? details.getLastModified()
235+
: CONSTANT_TIME_FOR_ZIP_ENTRIES;
277236
}
278237

279238
}
@@ -283,25 +242,25 @@ private long getTime(FileCopyDetails details) {
283242
*/
284243
private static final class Crc32OutputStream extends OutputStream {
285244

286-
private final CRC32 crc32 = new CRC32();
245+
private final CRC32 crc = new CRC32();
287246

288247
@Override
289248
public void write(int b) throws IOException {
290-
this.crc32.update(b);
249+
this.crc.update(b);
291250
}
292251

293252
@Override
294253
public void write(byte[] b) throws IOException {
295-
this.crc32.update(b);
254+
this.crc.update(b);
296255
}
297256

298257
@Override
299258
public void write(byte[] b, int off, int len) throws IOException {
300-
this.crc32.update(b, off, len);
259+
this.crc.update(b, off, len);
301260
}
302261

303262
private long getCrc() {
304-
return this.crc32.getValue();
263+
return this.crc.getValue();
305264
}
306265

307266
}

0 commit comments

Comments
 (0)