Favour entries in source jar over standard libraries when repackaging

When writing a jar, once an entry has been written it will never be
overwritten, i.e. the first write of a given entry will win. Previously,
when repackaging a jar, the existing contents were written followed by
any libraries. This caused a problem when repackaged a WAR file and
a library needed to be unpacked as the existing entry in WEB-INF/lib
would prevent the library with the UNPACK comment from being written.
This was addressed in f761916b by inverting the order so libraries
would take precedence over entries in the source jar.

It’s now become apparent that this change in the order causes a problem
for users who are obfuscating their code. The obfuscated code exists in
the source jar but is also provided to the repackager in its original
form as a library. When libraries take precedence, this means that the
code in its original form ends up in the repackaged war and the
obfuscation is lost.

This commit updates the repackager to write libraries that require
unpacking first. This allows the UNPACK comment to be written even if
there’s also a source entry for the library. Next, source entries are
written. This allows obfuscated source entries to take precedence over
any unobfuscated library equivalents. Lastly, standard libraries that
do not require unpacking are written into the repackaged archive.

Closes gh-3444
This commit is contained in:
Andy Wilkinson 2015-07-09 16:24:09 +01:00
parent 7da808918f
commit 68e54e1d5d
2 changed files with 94 additions and 12 deletions

View File

@ -20,7 +20,9 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
@ -154,29 +156,30 @@ public class Repackager {
private void repackage(JarFile sourceJar, File destination, Libraries libraries)
throws IOException {
final JarWriter writer = new JarWriter(destination);
JarWriter writer = new JarWriter(destination);
try {
final Set<String> seen = new HashSet<String>();
writer.writeManifest(buildManifest(sourceJar));
final List<Library> unpackLibraries = new ArrayList<Library>();
final List<Library> standardLibraries = new ArrayList<Library>();
libraries.doWithLibraries(new LibraryCallback() {
@Override
public void library(Library library) throws IOException {
File file = library.getFile();
if (isZip(file)) {
String destination = Repackager.this.layout
.getLibraryDestination(library.getName(),
library.getScope());
if (destination != null) {
if (!seen.add(destination + library.getName())) {
throw new IllegalStateException("Duplicate library "
+ library.getName());
}
writer.writeNestedLibrary(destination, library);
if (library.isUnpackRequired()) {
unpackLibraries.add(library);
}
else {
standardLibraries.add(library);
}
}
}
});
writer.writeManifest(buildManifest(sourceJar));
Set<String> seen = new HashSet<String>();
writeNestedLibraries(unpackLibraries, seen, writer);
writer.writeEntries(sourceJar);
writeNestedLibraries(standardLibraries, seen, writer);
if (this.layout.isExecutable()) {
writer.writeLoaderClasses();
}
@ -191,6 +194,21 @@ public class Repackager {
}
}
private void writeNestedLibraries(List<Library> libraries, Set<String> alreadySeen,
JarWriter writer) throws IOException {
for (Library library : libraries) {
String destination = Repackager.this.layout.getLibraryDestination(
library.getName(), library.getScope());
if (destination != null) {
if (!alreadySeen.add(destination + library.getName())) {
throw new IllegalStateException("Duplicate library "
+ library.getName());
}
writer.writeNestedLibrary(destination, library);
}
}
}
private boolean isZip(File file) {
try {
FileInputStream fileInputStream = new FileInputStream(file);

View File

@ -32,6 +32,7 @@ import org.junit.rules.TemporaryFolder;
import org.springframework.boot.loader.tools.sample.ClassWithMainMethod;
import org.springframework.boot.loader.tools.sample.ClassWithoutMainMethod;
import org.springframework.util.FileCopyUtils;
import org.zeroturnaround.zip.ZipUtil;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.startsWith;
@ -393,6 +394,69 @@ public class RepackagerTests {
}
}
@Test
public void unpackLibrariesTakePrecedenceOverExistingSourceEntries() throws Exception {
final TestJarFile nested = new TestJarFile(this.temporaryFolder);
nested.addClass("a/b/C.class", ClassWithoutMainMethod.class);
final File nestedFile = nested.getFile();
this.testJarFile.addFile("lib/" + nestedFile.getName(), nested.getFile());
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
File file = this.testJarFile.getFile();
Repackager repackager = new Repackager(file);
repackager.repackage(new Libraries() {
@Override
public void doWithLibraries(LibraryCallback callback) throws IOException {
callback.library(new Library(nestedFile, LibraryScope.COMPILE, true));
}
});
JarFile jarFile = new JarFile(file);
try {
assertThat(jarFile.getEntry("lib/" + nestedFile.getName()).getComment(),
startsWith("UNPACK:"));
}
finally {
jarFile.close();
}
}
@Test
public void existingSourceEntriesTakePrecedenceOverStandardLibraries()
throws Exception {
final TestJarFile nested = new TestJarFile(this.temporaryFolder);
nested.addClass("a/b/C.class", ClassWithoutMainMethod.class);
final File nestedFile = nested.getFile();
this.testJarFile.addFile("lib/" + nestedFile.getName(), nested.getFile());
this.testJarFile.addClass("A.class", ClassWithMainMethod.class);
File file = this.testJarFile.getFile();
Repackager repackager = new Repackager(file);
long sourceLength = nestedFile.length();
repackager.repackage(new Libraries() {
@Override
public void doWithLibraries(LibraryCallback callback) throws IOException {
nestedFile.delete();
File toZip = RepackagerTests.this.temporaryFolder.newFile();
ZipUtil.packEntry(toZip, nestedFile);
callback.library(new Library(nestedFile, LibraryScope.COMPILE));
}
});
JarFile jarFile = new JarFile(file);
try {
assertThat(jarFile.getEntry("lib/" + nestedFile.getName()).getSize(),
equalTo(sourceLength));
}
finally {
jarFile.close();
}
}
private boolean hasLauncherClasses(File file) throws IOException {
return hasEntry(file, "org/springframework/boot/")
&& hasEntry(file, "org/springframework/boot/loader/JarLauncher.class");