Ensure that fat jars and wars do not corrupt UTF-8 entry names

Previously, both Repackager and the Grade plugin used the JRE's
standard ZipOutputStream when creating a fat jar or war file. This
resulted in entry names that needed UTF-8 encoding to become
corrupted.

This commit updates both to use Commons Compress'
ZipArchiveOutputStream and to configure the stream's encoding and
each entry's Unix mode. This ensures that names are encoded using
UTF-8 and can be read back in correctly by common zip tools.

Closes gh-9405
This commit is contained in:
Andy Wilkinson
2017-06-22 07:39:50 -07:00
parent 885e29934b
commit f0b7e7cf56
11 changed files with 137 additions and 54 deletions

View File

@@ -83,7 +83,7 @@ class BootArchiveSupport {
CopyAction copyAction = new BootZipCopyAction(jar.getArchivePath(),
jar.isPreserveFileTimestamps(), isUsingDefaultLoader(jar),
this.requiresUnpack.getAsSpec(), this.exclusions.getAsExcludeSpec(),
this.launchScript, this.compressionResolver);
this.launchScript, this.compressionResolver, jar.getMetadataCharset());
if (!jar.isReproducibleFileOrder()) {
return copyAction;
}

View File

@@ -24,10 +24,11 @@ import java.util.HashSet;
import java.util.Set;
import java.util.function.Function;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
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;
@@ -65,10 +66,13 @@ class BootZipCopyAction implements CopyAction {
private final Function<FileCopyDetails, ZipCompression> compressionResolver;
private final String encoding;
BootZipCopyAction(File output, boolean preserveFileTimestamps,
boolean includeDefaultLoader, Spec<FileTreeElement> requiresUnpack,
Spec<FileTreeElement> exclusions, LaunchScriptConfiguration launchScript,
Function<FileCopyDetails, ZipCompression> compressionResolver) {
Function<FileCopyDetails, ZipCompression> compressionResolver,
String encoding) {
this.output = output;
this.preserveFileTimestamps = preserveFileTimestamps;
this.includeDefaultLoader = includeDefaultLoader;
@@ -76,16 +80,20 @@ class BootZipCopyAction implements CopyAction {
this.exclusions = exclusions;
this.launchScript = launchScript;
this.compressionResolver = compressionResolver;
this.encoding = encoding;
}
@Override
public WorkResult execute(CopyActionProcessingStream stream) {
ZipOutputStream zipStream;
ZipArchiveOutputStream zipStream;
Spec<FileTreeElement> loaderEntries;
try {
FileOutputStream fileStream = new FileOutputStream(this.output);
writeLaunchScriptIfNecessary(fileStream);
zipStream = new ZipOutputStream(fileStream);
zipStream = new ZipArchiveOutputStream(fileStream);
if (this.encoding != null) {
zipStream.setEncoding(this.encoding);
}
loaderEntries = writeLoaderClassesIfNecessary(zipStream);
}
catch (IOException ex) {
@@ -113,25 +121,26 @@ class BootZipCopyAction implements CopyAction {
return Specs.union(loaderEntries, this.exclusions);
}
private Spec<FileTreeElement> writeLoaderClassesIfNecessary(ZipOutputStream out) {
private Spec<FileTreeElement> writeLoaderClassesIfNecessary(
ZipArchiveOutputStream out) {
if (!this.includeDefaultLoader) {
return Specs.satisfyNone();
}
return writeLoaderClasses(out);
}
private Spec<FileTreeElement> writeLoaderClasses(ZipOutputStream 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<String>();
ZipEntry entry;
java.util.zip.ZipEntry entry;
while ((entry = in.getNextEntry()) != null) {
if (entry.isDirectory() && !entry.getName().startsWith("META-INF/")) {
writeDirectory(entry, out);
writeDirectory(new ZipArchiveEntry(entry), out);
entries.add(entry.getName());
}
else if (entry.getName().endsWith(".class")) {
writeClass(entry, in, out);
writeClass(new ZipArchiveEntry(entry), in, out);
}
}
return (element) -> {
@@ -147,26 +156,29 @@ class BootZipCopyAction implements CopyAction {
}
}
private void writeDirectory(ZipEntry entry, ZipOutputStream out) throws IOException {
if (!this.preserveFileTimestamps) {
entry.setTime(GUtil.CONSTANT_TIME_FOR_ZIP_ENTRIES);
}
out.putNextEntry(entry);
out.closeEntry();
}
private void writeClass(ZipEntry entry, ZipInputStream in, ZipOutputStream out)
private void writeDirectory(ZipArchiveEntry entry, ZipArchiveOutputStream out)
throws IOException {
if (!this.preserveFileTimestamps) {
entry.setTime(GUtil.CONSTANT_TIME_FOR_ZIP_ENTRIES);
}
out.putNextEntry(entry);
entry.setUnixMode(UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM);
out.putArchiveEntry(entry);
out.closeArchiveEntry();
}
private void writeClass(ZipArchiveEntry entry, ZipInputStream in,
ZipArchiveOutputStream out) throws IOException {
if (!this.preserveFileTimestamps) {
entry.setTime(GUtil.CONSTANT_TIME_FOR_ZIP_ENTRIES);
}
entry.setUnixMode(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);
}
out.closeEntry();
out.closeArchiveEntry();
}
private void writeLaunchScriptIfNecessary(FileOutputStream fileStream) {
@@ -185,7 +197,7 @@ class BootZipCopyAction implements CopyAction {
private static final class ZipStreamAction
implements CopyActionProcessingStreamAction {
private final ZipOutputStream zipStream;
private final ZipArchiveOutputStream zipStream;
private final File output;
@@ -197,7 +209,7 @@ class BootZipCopyAction implements CopyAction {
private final Function<FileCopyDetails, ZipCompression> compressionType;
private ZipStreamAction(ZipOutputStream zipStream, File output,
private ZipStreamAction(ZipArchiveOutputStream zipStream, File output,
boolean preserveFileTimestamps, Spec<FileTreeElement> requiresUnpack,
Spec<FileTreeElement> exclusions,
Function<FileCopyDetails, ZipCompression> compressionType) {
@@ -229,29 +241,31 @@ class BootZipCopyAction implements CopyAction {
}
private void createDirectory(FileCopyDetailsInternal details) throws IOException {
ZipEntry archiveEntry = new ZipEntry(
ZipArchiveEntry archiveEntry = new ZipArchiveEntry(
details.getRelativePath().getPathString() + '/');
archiveEntry.setUnixMode(UnixStat.DIR_FLAG | details.getMode());
archiveEntry.setTime(getTime(details));
this.zipStream.putNextEntry(archiveEntry);
this.zipStream.closeEntry();
this.zipStream.putArchiveEntry(archiveEntry);
this.zipStream.closeArchiveEntry();
}
private void createFile(FileCopyDetailsInternal details) throws IOException {
String relativePath = details.getRelativePath().getPathString();
ZipEntry archiveEntry = new ZipEntry(relativePath);
ZipArchiveEntry archiveEntry = new ZipArchiveEntry(relativePath);
archiveEntry.setUnixMode(UnixStat.FILE_FLAG | details.getMode());
archiveEntry.setTime(getTime(details));
ZipCompression compression = this.compressionType.apply(details);
if (compression == ZipCompression.STORED) {
prepareStoredEntry(details, archiveEntry);
}
this.zipStream.putNextEntry(archiveEntry);
this.zipStream.putArchiveEntry(archiveEntry);
details.copyTo(this.zipStream);
this.zipStream.closeEntry();
this.zipStream.closeArchiveEntry();
}
private void prepareStoredEntry(FileCopyDetailsInternal details,
ZipEntry archiveEntry) throws IOException {
archiveEntry.setMethod(ZipEntry.STORED);
ZipArchiveEntry archiveEntry) throws IOException {
archiveEntry.setMethod(java.util.zip.ZipEntry.STORED);
archiveEntry.setSize(details.getSize());
archiveEntry.setCompressedSize(details.getSize());
Crc32OutputStream crcStream = new Crc32OutputStream();

View File

@@ -27,6 +27,8 @@ import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.gradle.api.Project;
import org.gradle.api.tasks.bundling.AbstractArchiveTask;
import org.gradle.api.tasks.bundling.Jar;
@@ -306,6 +308,27 @@ public abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
}
}
@Test
public void allEntriesUseUnixPlatformAndUtf8NameEncoding() throws IOException {
this.task.setMainClass("com.example.Main");
this.task.setMetadataCharset("UTF-8");
File classpathFolder = this.temp.newFolder();
File resource = new File(classpathFolder, "some-resource.xml");
resource.getParentFile().mkdirs();
resource.createNewFile();
this.task.classpath(classpathFolder);
this.task.execute();
File archivePath = this.task.getArchivePath();
try (ZipFile zip = new ZipFile(archivePath)) {
Enumeration<ZipArchiveEntry> entries = zip.getEntries();
while (entries.hasMoreElements()) {
ZipArchiveEntry entry = entries.nextElement();
assertThat(entry.getPlatform()).isEqualTo(ZipArchiveEntry.PLATFORM_UNIX);
assertThat(entry.getGeneralPurposeBit().usesUTF8ForNames()).isTrue();
}
}
}
private T configure(T task) throws IOException {
AbstractArchiveTask archiveTask = task;
archiveTask.setBaseName("test");

View File

@@ -30,6 +30,7 @@ import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;
import io.spring.gradle.dependencymanagement.DependencyManagementPlugin;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.GradleRunner;
import org.junit.rules.TemporaryFolder;
@@ -114,7 +115,8 @@ public class GradleBuild implements TestRule {
+ absolutePath("build/resources/main") + ","
+ pathOfJarContaining(LaunchScript.class) + ","
+ pathOfJarContaining(ClassVisitor.class) + ","
+ pathOfJarContaining(DependencyManagementPlugin.class);
+ pathOfJarContaining(DependencyManagementPlugin.class) + ","
+ pathOfJarContaining(ArchiveEntry.class);
}
private String absolutePath(String path) {