Created
November 10, 2020 15:39
-
-
Save tkuenneth/ef27ca7abb4284a22f8c9f1ee6babdd6 to your computer and use it in GitHub Desktop.
Determines and visualizes dependencies between classes belonging to an Android app
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* DexAnalyzer.java | |
* Copyright 2016 Thomas Kuenneth | |
* | |
* This program is free software: you can redistribute it and/or modify | |
* it under the terms of the GNU General Public License version 3 | |
* as published by the Free Software Foundation. | |
* | |
* This program is distributed in the hope that it will be useful, | |
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
* GNU General Public License for more details. | |
* | |
* You should have received a copy of the GNU General Public License | |
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |
*/ | |
package com.thomaskuenneth.dexanalyzer; | |
import java.io.BufferedReader; | |
import java.io.File; | |
import java.io.FileReader; | |
import java.io.IOException; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.HashMap; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.logging.Level; | |
import java.util.logging.Logger; | |
import java.util.regex.Matcher; | |
import java.util.regex.Pattern; | |
/** | |
* This program determines and visualizes dependencies between classes that | |
* belong to an Android app. DexAnalyzer analyzes text files that have been | |
* created this way:<br> | |
* <code>dexdump -l plain -f classes.dex > classes.txt</code> | |
* | |
* @author Thomas Kuenneth | |
*/ | |
public class DexAnalyzer { | |
private static final String IGNORE = "-ignore="; | |
private static final String GLUE = "-glue="; | |
private static final Logger LOGGER = Logger.getLogger(DexAnalyzer.class.getName()); | |
private static final Pattern SIMPLE_PATTERN = Pattern.compile("^.*'\\[?L(.*);'.*$", Pattern.DOTALL); | |
private static final Pattern COMPLEX_PATTERN = Pattern.compile("^.*\\((.*)\\)(.*)'.*$", Pattern.DOTALL); | |
private final String[] ignores; | |
private final String[] glue; | |
private final boolean keepLastPortion; | |
private final boolean verbose; | |
private final boolean includeSelfReference; | |
private final Map<String, Map<String, Integer>> dependencies; | |
private String currentKey; | |
private enum STATE { | |
WAITING, CLASS, INTERFACES, FIELDS, METHODS | |
} | |
public DexAnalyzer() { | |
this(new String[]{}, new String[]{}, false, false, false); | |
} | |
public DexAnalyzer(String[] ignores, String[] glue, | |
boolean keepLastPortion, boolean verbose, boolean includeSelfReference) { | |
dependencies = new HashMap<>(); | |
this.ignores = ignores; | |
this.glue = glue; | |
this.keepLastPortion = keepLastPortion; | |
this.verbose = verbose; | |
this.includeSelfReference = includeSelfReference; | |
} | |
public void analyze(String filename) { | |
dependencies.clear(); | |
STATE state = STATE.WAITING; | |
File f = new File(filename); | |
try (FileReader fin = new FileReader(f); | |
BufferedReader br = new BufferedReader(fin)) { | |
String line; | |
while ((line = br.readLine()) != null) { | |
if (line.contains("Class #")) { | |
state = STATE.CLASS; | |
} else if ((line.contains("Class descriptor")) && (state == STATE.CLASS)) { | |
Matcher m = SIMPLE_PATTERN.matcher(line); | |
if (m.matches()) { | |
String fullyQualifiedClassName = replaceAllSlashesWithColons(m.group(1)); | |
printVerbose(String.format("found class %s", | |
fullyQualifiedClassName)); | |
String key = createKey(fullyQualifiedClassName); | |
if (shouldUseKey(key)) { | |
if (!dependencies.containsKey(key)) { | |
dependencies.put(key, new HashMap<>()); | |
printVerbose(String.format("new dependency: %s", key)); | |
} | |
} | |
currentKey = key; | |
} | |
} else if (line.contains("Superclass :")) { | |
handleSimpleLine(line); | |
} else if (line.contains("Interfaces -")) { | |
state = STATE.INTERFACES; | |
} else if (line.contains("Static fields -") | |
|| line.contains("Instance fields -")) { | |
state = STATE.FIELDS; | |
} else if (line.contains("Direct methods -") | |
|| line.contains("Virtual methods -")) { | |
state = STATE.METHODS; | |
} else if (line.contains("type :") | |
&& (state == STATE.FIELDS)) { | |
handleSimpleLine(line); | |
} else if (line.contains("#") | |
&& (state == STATE.INTERFACES)) { | |
handleSimpleLine(line); | |
} else if (line.contains("type :") | |
&& (state == STATE.METHODS)) { | |
handleMethodLine(line); | |
} | |
// TODO: we need to include catches | |
} | |
} catch (IOException ex) { | |
LOGGER.log(Level.SEVERE, "analyze()", ex); | |
} | |
} | |
public void displayResultsAsTGF() { | |
// map keys to indices | |
Map<String, Integer> indices = new HashMap<>(); | |
dependencies.forEach((keyDependencies, valueDependencies) -> { | |
addToMapIfKeyNotPresent(indices, keyDependencies); | |
valueDependencies.forEach((keyDependency, valueDependency) -> { | |
addToMapIfKeyNotPresent(indices, keyDependency); | |
}); | |
}); | |
// output the Trivial Graph Format (TGF) | |
indices.forEach((key, index) -> { | |
print(String.format("%d %s", index, key)); | |
}); | |
print("#"); | |
dependencies.forEach((keyDependencies, valueDependencies) -> { | |
int index = indices.get(keyDependencies); | |
valueDependencies.forEach((keyDependency, valueDependency) -> { | |
print(String.format("%d %d", index, indices.get(keyDependency))); | |
}); | |
}); | |
} | |
private void addToMapIfKeyNotPresent(Map<String, Integer> map, String key) { | |
if (!map.containsKey(key)) { | |
int index = 1 + map.size(); | |
map.put(key, index); | |
} | |
} | |
public void displayResults() { | |
print("----------------------------------------"); | |
print(String.format("Displaying dependencies for %d items", dependencies.size())); | |
print("----------------------------------------"); | |
String[] a = new String[dependencies.size()]; | |
dependencies.keySet().toArray(a); | |
Arrays.sort(a); | |
for (String key : a) { | |
Map<String, Integer> value = dependencies.get(key); | |
print(key); | |
String[] abc = new String[value.size()]; | |
value.keySet().toArray(abc); | |
Arrays.sort(abc); | |
for (String key2 : abc) { | |
print(String.format(" %s", key2)); | |
} | |
} | |
} | |
private String replaceAllSlashesWithColons(String in) { | |
return in.replace('/', '.'); | |
} | |
private String createKey(String in) { | |
for (String g : glue) { | |
if (in.startsWith(g)) { | |
in = g; | |
break; | |
} | |
} | |
int pos = in.lastIndexOf("."); | |
if ((pos < 0) || keepLastPortion) { | |
pos = in.length(); | |
} | |
return in.substring(0, pos); | |
} | |
private void handleSimpleLine(String line) { | |
Matcher m = SIMPLE_PATTERN.matcher(line); | |
if (m.matches()) { | |
add(m.group(1)); | |
} | |
} | |
private void handleMethodLine(String line) { | |
Matcher m = COMPLEX_PATTERN.matcher(line); | |
if (m.matches()) { | |
String[] params = m.group(1).split(";"); | |
add(params); | |
String[] result = m.group(2).split(";"); | |
add(result); | |
} | |
} | |
/** | |
* Adds the contens of an array. We are ignoring primitive types | |
* | |
* @param array the array | |
*/ | |
private void add(String[] array) { | |
for (String s : array) { | |
if (s.startsWith("L")) { | |
add(replaceAllSlashesWithColons(s.substring(1))); | |
} | |
} | |
} | |
private void add(String s) { | |
Map<String, Integer> map = dependencies.get(currentKey); | |
if (map != null) { | |
String fullyQualifiedClassName = replaceAllSlashesWithColons(s); | |
String key = createKey(fullyQualifiedClassName); | |
if (shouldUseKey(key)) { | |
if (includeSelfReference || !currentKey.equals(key)) { | |
if (!map.containsKey(key)) { | |
map.put(key, 0); | |
printVerbose(String.format("%s needs %s", currentKey, key)); | |
} | |
int count = 1 + map.get(key); | |
map.put(key, count); | |
} | |
} | |
} | |
} | |
private boolean shouldUseKey(String key) { | |
for (String s : ignores) { | |
if (key.startsWith(s)) { | |
return false; | |
} | |
} | |
return true; | |
} | |
private void print(String s) { | |
System.out.println(s); | |
} | |
private void printVerbose(String s) { | |
if (verbose) { | |
print(s); | |
} | |
} | |
/** | |
* Main entry point | |
* | |
* @param args the command line arguments | |
*/ | |
public static void main(String[] args) { | |
boolean keepLastPortion = false; | |
boolean verbose = false; | |
boolean includeSelfReference = false; | |
boolean createTGF = false; | |
String[] ignores = {}; | |
String[] glue = {}; | |
List<String> files = new ArrayList<>(); | |
for (String s : args) { | |
switch (s) { | |
case "-keepLastPortion": | |
keepLastPortion = true; | |
break; | |
case "-verbose": | |
verbose = true; | |
break; | |
case "-includeSelfReference": | |
includeSelfReference = true; | |
break; | |
case "-createTGF": | |
createTGF = true; | |
break; | |
default: | |
if (s.startsWith(GLUE)) { | |
if (s.length() > GLUE.length()) { | |
String str = s.substring(GLUE.length()); | |
glue = str.split("\\|"); | |
} | |
} else if (s.startsWith(IGNORE)) { | |
if (s.length() > IGNORE.length()) { | |
String str = s.substring(IGNORE.length()); | |
ignores = str.split("\\|"); | |
} | |
} else { | |
files.add(s); | |
} | |
break; | |
} | |
final boolean _createTGF = createTGF; | |
DexAnalyzer me = new DexAnalyzer(ignores, glue, | |
keepLastPortion, verbose, includeSelfReference); | |
files.forEach(filename -> { | |
me.analyze(filename); | |
if (_createTGF) { | |
me.displayResultsAsTGF(); | |
} else { | |
me.displayResults(); | |
} | |
}); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment