|  | // 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; | 
|  | } | 
|  | } |