| // Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| package com.android.tools.r8; |
| |
| import com.android.tools.r8.cf.code.CfInstruction; |
| import com.android.tools.r8.graph.ClassKind; |
| import com.android.tools.r8.graph.DexEncodedMethod; |
| import com.android.tools.r8.graph.DexProgramClass; |
| import com.android.tools.r8.graph.JarApplicationReader; |
| import com.android.tools.r8.graph.JarClassFileReader; |
| import com.android.tools.r8.origin.PathOrigin; |
| import com.android.tools.r8.utils.Box; |
| import com.android.tools.r8.utils.InternalOptions; |
| import com.android.tools.r8.utils.StreamUtils; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.stream.Collectors; |
| |
| /** |
| * Command-line program to compare two JARs. Given two JARs as input, the program first outputs a |
| * list of classes only in one of the input JARs. Then, for each common class, the program outputs a |
| * list of methods only in one of the input JARs. For each common method, the output of |
| * CfInstruction.toString() on each instruction is compared to find instruction-level differences. A |
| * simple diffing algorithm is used that simply removes the common prefix and common suffix and |
| * prints everything from the first difference to the last difference in the method code. |
| */ |
| public class JarDiff { |
| |
| private static final String USAGE = |
| "Arguments: <input1.jar> <input2.jar>\n" |
| + "\n" |
| + "JarDiff computes the difference between two JAR files that contain Java classes.\n" |
| + "\n" |
| + "Only method codes are compared. Fields, parameters, annotations, generic\n" |
| + "signatures etc. are ignored.\n" |
| + "\n" |
| + "Note: Jump targets are ignored, so if two methods differ only in what label an\n" |
| + "IF or GOTO instruction jumps to, no difference is output."; |
| |
| public static void main(String[] args) throws Exception { |
| JarDiff jarDiff = JarDiff.parse(args); |
| if (jarDiff == null) { |
| System.out.println(USAGE); |
| } else { |
| jarDiff.run(); |
| } |
| } |
| |
| public static JarDiff parse(String[] args) { |
| int arg = 0; |
| int before = 3; |
| int after = 3; |
| while (arg + 1 < args.length) { |
| if (args[arg].equals("-B")) { |
| before = Integer.parseInt(args[arg + 1]); |
| arg += 2; |
| } else if (args[arg].equals("-A")) { |
| after = Integer.parseInt(args[arg + 1]); |
| arg += 2; |
| } else { |
| break; |
| } |
| } |
| if (args.length != arg + 2) { |
| return null; |
| } |
| return new JarDiff(Paths.get(args[arg]), Paths.get(args[arg + 1]), before, after); |
| } |
| |
| private final Path path1; |
| private final Path path2; |
| private final int before; |
| private final int after; |
| private final JarApplicationReader applicationReader; |
| private ArchiveClassFileProvider archive1; |
| private ArchiveClassFileProvider archive2; |
| |
| public JarDiff(Path path1, Path path2, int before, int after) { |
| this.path1 = path1; |
| this.path2 = path2; |
| this.before = before; |
| this.after = after; |
| InternalOptions options = new InternalOptions(); |
| applicationReader = new JarApplicationReader(options); |
| } |
| |
| public void run() throws Exception { |
| archive1 = new ArchiveClassFileProvider(path1); |
| archive2 = new ArchiveClassFileProvider(path2); |
| for (String descriptor : getCommonDescriptors()) { |
| byte[] bytes1 = getClassAsBytes(archive1, descriptor); |
| byte[] bytes2 = getClassAsBytes(archive2, descriptor); |
| if (Arrays.equals(bytes1, bytes2)) { |
| continue; |
| } |
| DexProgramClass class1 = getDexProgramClass(path1, bytes1); |
| DexProgramClass class2 = getDexProgramClass(path2, bytes2); |
| compareMethods(class1, class2); |
| } |
| } |
| |
| private List<String> getCommonDescriptors() { |
| List<String> descriptors1 = getSortedDescriptorList(archive1); |
| List<String> descriptors2 = getSortedDescriptorList(archive2); |
| if (descriptors1.equals(descriptors2)) { |
| return descriptors1; |
| } |
| List<String> only1 = setMinus(descriptors1, descriptors2); |
| List<String> only2 = setMinus(descriptors2, descriptors1); |
| if (!only1.isEmpty()) { |
| System.out.println("Only in " + path1 + ": " + only1); |
| } |
| if (!only2.isEmpty()) { |
| System.out.println("Only in " + path2 + ": " + only2); |
| } |
| return setIntersection(descriptors1, descriptors2); |
| } |
| |
| private List<String> getSortedDescriptorList(ArchiveClassFileProvider inputJar) { |
| ArrayList<String> descriptorList = new ArrayList<>(inputJar.getClassDescriptors()); |
| Collections.sort(descriptorList); |
| return descriptorList; |
| } |
| |
| private byte[] getClassAsBytes(ArchiveClassFileProvider inputJar, String descriptor) |
| throws Exception { |
| return StreamUtils.StreamToByteArrayClose( |
| inputJar.getProgramResource(descriptor).getByteStream()); |
| } |
| |
| private DexProgramClass getDexProgramClass(Path path, byte[] bytes) { |
| Box<DexProgramClass> collector = new Box<>(); |
| JarClassFileReader<DexProgramClass> reader = |
| new JarClassFileReader<>(applicationReader, collector::set, ClassKind.PROGRAM); |
| reader.read(new PathOrigin(path), bytes); |
| return collector.get().asProgramClass(); |
| } |
| |
| private void compareMethods(DexProgramClass class1, DexProgramClass class2) { |
| class1.forEachMethod( |
| method1 -> { |
| DexEncodedMethod method2 = class2.lookupMethod(method1.getReference()); |
| compareMethods(method1, method2); |
| }); |
| class2.forEachMethod( |
| method2 -> { |
| DexEncodedMethod method1 = class1.lookupMethod(method2.getReference()); |
| compareMethods(method1, method2); |
| }); |
| } |
| |
| private void compareMethods(DexEncodedMethod m1, DexEncodedMethod m2) { |
| if (m1 == null) { |
| System.out.println("Only in " + path2 + ": " + m2.getReference().toSourceString()); |
| return; |
| } |
| if (m2 == null) { |
| System.out.println("Only in " + path1 + ": " + m1.getReference().toSourceString()); |
| return; |
| } |
| List<String> code1 = getInstructionStrings(m1); |
| List<String> code2 = getInstructionStrings(m2); |
| if (code1.equals(code2)) { |
| return; |
| } |
| int i = getCommonPrefix(code1, code2); |
| int j = getCommonSuffix(code1, code2); |
| int length1 = code1.size() - i - j; |
| int length2 = code2.size() - i - j; |
| int before = Math.min(i, this.before); |
| int after = Math.min(j, this.after); |
| int context = before + after; |
| System.out.println("--- " + path1 + "/" + m1.getReference().toSmaliString()); |
| System.out.println("+++ " + path2 + "/" + m2.getReference().toSmaliString()); |
| System.out.println( |
| "@@ -" |
| + (i - before) |
| + "," |
| + (length1 + context) |
| + " +" |
| + (i - before) |
| + "," |
| + (length2 + context) |
| + " @@ " |
| + m1.getReference().toSourceString()); |
| for (int k = 0; k < before; k++) { |
| System.out.println(" " + code1.get(i - before + k)); |
| } |
| for (int k = 0; k < length1; k++) { |
| System.out.println("-" + code1.get(i + k)); |
| } |
| for (int k = 0; k < length2; k++) { |
| System.out.println("+" + code2.get(i + k)); |
| } |
| for (int k = 0; k < after; k++) { |
| System.out.println(" " + code1.get(i + length1 + k)); |
| } |
| } |
| |
| private static List<String> getInstructionStrings(DexEncodedMethod method) { |
| List<CfInstruction> instructions = method.getCode().asCfCode().getInstructions(); |
| return instructions.stream().map(CfInstruction::toString).collect(Collectors.toList()); |
| } |
| |
| private static List<String> setIntersection(List<String> set1, List<String> set2) { |
| ArrayList<String> result = new ArrayList<>(set1); |
| result.retainAll(new HashSet<>(set2)); |
| return result; |
| } |
| |
| private static List<String> setMinus(List<String> set, List<String> toRemove) { |
| ArrayList<String> result = new ArrayList<>(set); |
| result.removeAll(new HashSet<>(toRemove)); |
| return result; |
| } |
| |
| private static int getCommonPrefix(List<String> code1, List<String> code2) { |
| int i = 0; |
| while (i < code1.size() && i < code2.size()) { |
| if (code1.get(i).equals(code2.get(i))) { |
| i++; |
| } else { |
| break; |
| } |
| } |
| return i; |
| } |
| |
| private static int getCommonSuffix(List<String> code1, List<String> code2) { |
| int j = 0; |
| while (j < code1.size() && j < code2.size()) { |
| if (code1.get(code1.size() - j - 1).equals(code2.get(code2.size() - j - 1))) { |
| j++; |
| } else { |
| break; |
| } |
| } |
| return j; |
| } |
| } |