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:
@@ -37,11 +37,14 @@ import java.util.Set;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.JarInputStream;
|
||||
import java.util.jar.JarOutputStream;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.zip.CRC32;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
import org.apache.commons.compress.archivers.jar.JarArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.jar.JarArchiveOutputStream;
|
||||
import org.apache.commons.compress.archivers.zip.UnixStat;
|
||||
|
||||
/**
|
||||
* Writes JAR content, ensuring valid directory entries are always create and duplicate
|
||||
* items are ignored.
|
||||
@@ -55,7 +58,7 @@ public class JarWriter implements LoaderClassesWriter, AutoCloseable {
|
||||
|
||||
private static final int BUFFER_SIZE = 32 * 1024;
|
||||
|
||||
private final JarOutputStream jarOutput;
|
||||
private final JarArchiveOutputStream jarOutput;
|
||||
|
||||
private final Set<String> writtenEntries = new HashSet<>();
|
||||
|
||||
@@ -83,7 +86,8 @@ public class JarWriter implements LoaderClassesWriter, AutoCloseable {
|
||||
fileOutputStream.write(launchScript.toByteArray());
|
||||
setExecutableFilePermission(file);
|
||||
}
|
||||
this.jarOutput = new JarOutputStream(fileOutputStream);
|
||||
this.jarOutput = new JarArchiveOutputStream(fileOutputStream);
|
||||
this.jarOutput.setEncoding("UTF-8");
|
||||
}
|
||||
|
||||
private void setExecutableFilePermission(File file) {
|
||||
@@ -105,7 +109,7 @@ public class JarWriter implements LoaderClassesWriter, AutoCloseable {
|
||||
* @throws IOException of the manifest cannot be written
|
||||
*/
|
||||
public void writeManifest(final Manifest manifest) throws IOException {
|
||||
JarEntry entry = new JarEntry("META-INF/MANIFEST.MF");
|
||||
JarArchiveEntry entry = new JarArchiveEntry("META-INF/MANIFEST.MF");
|
||||
writeEntry(entry, new EntryWriter() {
|
||||
@Override
|
||||
public void write(OutputStream outputStream) throws IOException {
|
||||
@@ -127,12 +131,12 @@ public class JarWriter implements LoaderClassesWriter, AutoCloseable {
|
||||
throws IOException {
|
||||
Enumeration<JarEntry> entries = jarFile.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
JarEntry entry = entries.nextElement();
|
||||
JarArchiveEntry entry = new JarArchiveEntry(entries.nextElement());
|
||||
setUpStoredEntryIfNecessary(jarFile, entry);
|
||||
try (ZipHeaderPeekInputStream inputStream = new ZipHeaderPeekInputStream(
|
||||
jarFile.getInputStream(entry))) {
|
||||
EntryWriter entryWriter = new InputStreamEntryWriter(inputStream, true);
|
||||
JarEntry transformedEntry = entryTransformer.transform(entry);
|
||||
JarArchiveEntry transformedEntry = entryTransformer.transform(entry);
|
||||
if (transformedEntry != null) {
|
||||
writeEntry(transformedEntry, entryWriter);
|
||||
}
|
||||
@@ -140,7 +144,7 @@ public class JarWriter implements LoaderClassesWriter, AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
private void setUpStoredEntryIfNecessary(JarFile jarFile, JarEntry entry)
|
||||
private void setUpStoredEntryIfNecessary(JarFile jarFile, JarArchiveEntry entry)
|
||||
throws IOException {
|
||||
try (ZipHeaderPeekInputStream inputStream = new ZipHeaderPeekInputStream(
|
||||
jarFile.getInputStream(entry))) {
|
||||
@@ -158,7 +162,7 @@ public class JarWriter implements LoaderClassesWriter, AutoCloseable {
|
||||
*/
|
||||
@Override
|
||||
public void writeEntry(String entryName, InputStream inputStream) throws IOException {
|
||||
JarEntry entry = new JarEntry(entryName);
|
||||
JarArchiveEntry entry = new JarArchiveEntry(entryName);
|
||||
writeEntry(entry, new InputStreamEntryWriter(inputStream, true));
|
||||
}
|
||||
|
||||
@@ -171,7 +175,7 @@ public class JarWriter implements LoaderClassesWriter, AutoCloseable {
|
||||
public void writeNestedLibrary(String destination, Library library)
|
||||
throws IOException {
|
||||
File file = library.getFile();
|
||||
JarEntry entry = new JarEntry(destination + library.getName());
|
||||
JarArchiveEntry entry = new JarArchiveEntry(destination + library.getName());
|
||||
entry.setTime(getNestedLibraryTime(file));
|
||||
if (library.isUnpackRequired()) {
|
||||
entry.setComment("UNPACK:" + FileUtils.sha1Hash(file));
|
||||
@@ -225,7 +229,8 @@ public class JarWriter implements LoaderClassesWriter, AutoCloseable {
|
||||
JarEntry entry;
|
||||
while ((entry = inputStream.getNextJarEntry()) != null) {
|
||||
if (entry.getName().endsWith(".class")) {
|
||||
writeEntry(entry, new InputStreamEntryWriter(inputStream, false));
|
||||
writeEntry(new JarArchiveEntry(entry),
|
||||
new InputStreamEntryWriter(inputStream, false));
|
||||
}
|
||||
}
|
||||
inputStream.close();
|
||||
@@ -247,24 +252,29 @@ public class JarWriter implements LoaderClassesWriter, AutoCloseable {
|
||||
* @param entryWriter the entry writer or {@code null} if there is no content
|
||||
* @throws IOException in case of I/O errors
|
||||
*/
|
||||
private void writeEntry(JarEntry entry, EntryWriter entryWriter) throws IOException {
|
||||
private void writeEntry(JarArchiveEntry entry, EntryWriter entryWriter)
|
||||
throws IOException {
|
||||
String parent = entry.getName();
|
||||
if (parent.endsWith("/")) {
|
||||
parent = parent.substring(0, parent.length() - 1);
|
||||
entry.setUnixMode(UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM);
|
||||
}
|
||||
else {
|
||||
entry.setUnixMode(UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM);
|
||||
}
|
||||
if (parent.lastIndexOf("/") != -1) {
|
||||
parent = parent.substring(0, parent.lastIndexOf("/") + 1);
|
||||
if (parent.length() > 0) {
|
||||
writeEntry(new JarEntry(parent), null);
|
||||
writeEntry(new JarArchiveEntry(parent), null);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.writtenEntries.add(entry.getName())) {
|
||||
this.jarOutput.putNextEntry(entry);
|
||||
this.jarOutput.putArchiveEntry(entry);
|
||||
if (entryWriter != null) {
|
||||
entryWriter.write(this.jarOutput);
|
||||
}
|
||||
this.jarOutput.closeEntry();
|
||||
this.jarOutput.closeArchiveEntry();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,7 +403,7 @@ public class JarWriter implements LoaderClassesWriter, AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
public void setupStoredEntry(JarEntry entry) {
|
||||
public void setupStoredEntry(JarArchiveEntry entry) {
|
||||
entry.setSize(this.size);
|
||||
entry.setCompressedSize(this.size);
|
||||
entry.setCrc(this.crc.getValue());
|
||||
@@ -408,7 +418,7 @@ public class JarWriter implements LoaderClassesWriter, AutoCloseable {
|
||||
*/
|
||||
interface EntryTransformer {
|
||||
|
||||
JarEntry transform(JarEntry jarEntry);
|
||||
JarArchiveEntry transform(JarArchiveEntry jarEntry);
|
||||
|
||||
}
|
||||
|
||||
@@ -418,7 +428,7 @@ public class JarWriter implements LoaderClassesWriter, AutoCloseable {
|
||||
private static final class IdentityEntryTransformer implements EntryTransformer {
|
||||
|
||||
@Override
|
||||
public JarEntry transform(JarEntry jarEntry) {
|
||||
public JarArchiveEntry transform(JarArchiveEntry jarEntry) {
|
||||
return jarEntry;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,10 +25,11 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import org.apache.commons.compress.archivers.jar.JarArchiveEntry;
|
||||
|
||||
import org.springframework.boot.loader.tools.JarWriter.EntryTransformer;
|
||||
import org.springframework.core.io.support.SpringFactoriesLoader;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -407,7 +408,7 @@ public class Repackager {
|
||||
}
|
||||
|
||||
@Override
|
||||
public JarEntry transform(JarEntry entry) {
|
||||
public JarArchiveEntry transform(JarArchiveEntry entry) {
|
||||
if (entry.getName().equals("META-INF/INDEX.LIST")) {
|
||||
return null;
|
||||
}
|
||||
@@ -416,7 +417,8 @@ public class Repackager {
|
||||
|| entry.getName().startsWith("BOOT-INF/")) {
|
||||
return entry;
|
||||
}
|
||||
JarEntry renamedEntry = new JarEntry(this.namePrefix + entry.getName());
|
||||
JarArchiveEntry renamedEntry = new JarArchiveEntry(
|
||||
this.namePrefix + entry.getName());
|
||||
renamedEntry.setTime(entry.getTime());
|
||||
renamedEntry.setSize(entry.getSize());
|
||||
renamedEntry.setMethod(entry.getMethod());
|
||||
|
||||
@@ -22,12 +22,15 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.util.Calendar;
|
||||
import java.util.Enumeration;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
@@ -595,6 +598,23 @@ public class RepackagerTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allEntriesUseUnixPlatformAndUtf8NameEncoding() throws IOException {
|
||||
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
|
||||
File source = this.testJarFile.getFile();
|
||||
File dest = this.temporaryFolder.newFile("dest.jar");
|
||||
Repackager repackager = new Repackager(source);
|
||||
repackager.repackage(dest, NO_LIBRARIES);
|
||||
try (ZipFile zip = new ZipFile(dest)) {
|
||||
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 boolean hasLauncherClasses(File file) throws IOException {
|
||||
return hasEntry(file, "org/springframework/boot/")
|
||||
&& hasEntry(file, "org/springframework/boot/loader/JarLauncher.class");
|
||||
|
||||
Reference in New Issue
Block a user