Skip to content

Instantly share code, notes, and snippets.

@tkuenneth
Created November 10, 2020 15:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tkuenneth/ef27ca7abb4284a22f8c9f1ee6babdd6 to your computer and use it in GitHub Desktop.
Save tkuenneth/ef27ca7abb4284a22f8c9f1ee6babdd6 to your computer and use it in GitHub Desktop.
Determines and visualizes dependencies between classes belonging to an Android app
/*
* 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 &gt; 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