diff --git a/lib/asm-util-9.6.jar b/lib/asm-util-9.6.jar new file mode 100644 index 0000000..cc109b0 Binary files /dev/null and b/lib/asm-util-9.6.jar differ diff --git a/src/main/com/tonicsystems/jarjar/util/EntryStruct.java b/src/main/com/tonicsystems/jarjar/util/EntryStruct.java index d52b10b..a73a071 100644 --- a/src/main/com/tonicsystems/jarjar/util/EntryStruct.java +++ b/src/main/com/tonicsystems/jarjar/util/EntryStruct.java @@ -27,10 +27,6 @@ public boolean isClass() { if (!name.endsWith(".class")) { return false; } - if (name.startsWith("META-INF/version")) { - // TODO(b/69678527): handle multi-release jar files - return false; - } return true; } diff --git a/src/main/com/tonicsystems/jarjar/util/JarTransformer.java b/src/main/com/tonicsystems/jarjar/util/JarTransformer.java index d109083..893800b 100644 --- a/src/main/com/tonicsystems/jarjar/util/JarTransformer.java +++ b/src/main/com/tonicsystems/jarjar/util/JarTransformer.java @@ -41,7 +41,7 @@ public boolean process(EntryStruct struct) throws IOException { } if (updateData) { struct.data = w.toByteArray(); - struct.name = pathFromName(w.getClassName()); + struct.name = replaceName(struct.name, w.getClassName()); } } return true; @@ -49,7 +49,11 @@ public boolean process(EntryStruct struct) throws IOException { protected abstract ClassVisitor transform(ClassVisitor v); - private static String pathFromName(String className) { - return className.replace('.', '/') + ".class"; + private static String replaceName(String name, String className) { + String prefix = + name.startsWith("META-INF/versions/") + ? name.substring(0, name.indexOf('/', "META-INF/versions/".length()) + 1) + : ""; + return prefix + className.replace('.', '/') + ".class"; } } diff --git a/src/test/com/tonicsystems/jarjar/util/StandaloneJarProcessorTest.java b/src/test/com/tonicsystems/jarjar/util/StandaloneJarProcessorTest.java index 42f86d5..e005211 100644 --- a/src/test/com/tonicsystems/jarjar/util/StandaloneJarProcessorTest.java +++ b/src/test/com/tonicsystems/jarjar/util/StandaloneJarProcessorTest.java @@ -19,6 +19,8 @@ import static java.nio.charset.StandardCharsets.UTF_8; import java.io.File; +import java.io.PrintWriter; +import java.io.StringWriter; import java.time.Instant; import java.util.ArrayList; import java.util.Enumeration; @@ -27,6 +29,14 @@ import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; import junit.framework.TestCase; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.commons.Remapper; +import org.objectweb.asm.util.Printer; +import org.objectweb.asm.util.Textifier; +import org.objectweb.asm.util.TraceClassVisitor; @SuppressWarnings("JdkImmutableCollections") public class StandaloneJarProcessorTest extends TestCase { @@ -128,6 +138,31 @@ public void testOutput_emptyDirsAreDeleted() throws Exception { createEntry("zaf/bar/A.class", "Hello"))); } + public void testProcessor_multiReleaseEntriesArePreserved() throws Exception { + assertJarTransformation( + List.of( + createEntry("foo/bar/A.class", createClass("foo/bar/A")), + createEntry("META-INF/versions/11/foo/bar/A.class", createClass("foo/bar/A")), + createEntry("META-INF/versions/21/foo/bar/A.class", createClass("foo/bar/A"))), + new JarTransformerChain( + new RemappingClassTransformer[] { + new RemappingClassTransformer( + new Remapper() { + @Override + public String map(final String internalName) { + if (internalName.equals("foo/bar/A")) { + return "foo/baz/B"; + } + return internalName; + } + }) + }), + List.of( + createEntry("META-INF/versions/11/foo/baz/B.class", createClass("foo/baz/B")), + createEntry("META-INF/versions/21/foo/baz/B.class", createClass("foo/baz/B")), + createEntry("foo/baz/B.class", createClass("foo/baz/B")))); + } + private void assertJarTransformation( List inEntries, JarProcessor processor, List expectedEntries) throws Exception { @@ -141,15 +176,33 @@ private void assertJarTransformation( EntryStruct actualEntry = actualEntries.get(i); assertEquals(expectedEntry.name, actualEntry.name); assertEquals(expectedEntry.time, actualEntry.time); - assertEquals(new String(expectedEntry.data, UTF_8), new String(actualEntry.data, UTF_8)); + assertEquals(printData(expectedEntry.data), printData(actualEntry.data)); } } + private String printData(byte[] data) { + Printer textifier = new Textifier(); + StringWriter sw = new StringWriter(); + try { + new ClassReader(data) + .accept( + new TraceClassVisitor(null, textifier, new PrintWriter(sw, true)), + ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES); + } catch (IndexOutOfBoundsException e) { + return new String(data, UTF_8); + } + return sw.toString(); + } + private EntryStruct createEntry(String name, String data) { + return createEntry(name, data.getBytes(UTF_8)); + } + + private EntryStruct createEntry(String name, byte[] data) { EntryStruct entry = new EntryStruct(); entry.name = name; entry.time = ARBITRARY_INSTANT.toEpochMilli(); - entry.data = data.getBytes(UTF_8); + entry.data = data; return entry; } @@ -181,6 +234,21 @@ private List readJar(File jar) throws Exception { return result; } + private byte[] createClass(String name) { + ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); + classWriter.visit(Opcodes.V1_8, Opcodes.ACC_SUPER, name, null, "java/lang/Object", null); + + MethodVisitor methodVisitor = classWriter.visitMethod(0, "", "()V", null, null); + methodVisitor.visitCode(); + methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); + methodVisitor.visitMethodInsn( + Opcodes.INVOKESPECIAL, "java/lang/Object", "", "()V", false); + methodVisitor.visitInsn(Opcodes.RETURN); + methodVisitor.visitEnd(); + + return classWriter.toByteArray(); + } + private static final Instant ARBITRARY_INSTANT = Instant.parse("2024-02-27T10:15:30.00Z"); public StandaloneJarProcessorTest(String name) {