diff --git a/.gitignore b/.gitignore index 3565259..dcff21b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,7 @@ apk-dependency-graph-scripts-*.zip .ant .gradle # lib/ directory where the dependencies are downloaded by Ivy Apache. -lib \ No newline at end of file +lib +filters/*.json +!filters/default.json +.vscode diff --git a/.travis.yml b/.travis.yml index 82a854e..7de0dbc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,17 +3,18 @@ jdk: openjdk8 install: skip -before_script: chmod +x ./gradlew -script: ./gradlew build +before_script: chmod +x ./gradlew # Get Permissions +script: ./gradlew jar test clean # Compile, Test, Clean excluding .jar branches: only: - master # master + - /(feature/).*/ # for features - /[0-9]+\.[0-9]+\.[0-9]+(.*)?/ # TAG before_deploy: # Create Archive - - rm -fr build/classes/ - - zip -r apk-dependency-graph-scripts-$TRAVIS_TAG.zip run.bat run.sh lib/ gui/ build/ +# - rm -fr build/classes/ # Uncomment line if used ant for buld + - zip -r apk-dependency-graph-scripts-$TRAVIS_TAG.zip run.bat run.sh lib/ gui/ build/ filters/ deploy: provider: releases api_key: $GITHUB_TOKEN # More options > Settings > Environment Variables diff --git a/README.md b/README.md index 3c4cf49..d0fcc5a 100644 --- a/README.md +++ b/README.md @@ -1,109 +1,67 @@ # Apk Dependency Graph [![Build Status](https://travis-ci.org/alexzaitsev/apk-dependency-graph.svg?branch=master)](https://travis-ci.org/alexzaitsev/apk-dependency-graph) -[![version](https://img.shields.io/badge/version-0.1.5-brightgreen.svg)](https://github.com/alexzaitsev/apk-dependency-graph/releases/tag/0.1.5) [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-apk--dependency--graph-blue.svg?style=flat)](http://android-arsenal.com/details/1/4411) +[![version](https://img.shields.io/badge/version-0.2.0-brightgreen.svg)](https://github.com/alexzaitsev/apk-dependency-graph/releases/tag/0.2.0) [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-apk--dependency--graph-blue.svg?style=flat)](http://android-arsenal.com/details/1/4411) -Android dependency visualizer. It's a tool that helps to visualize current state of your project. It's really easy to see how tight your classes are coupled. +Class dependency visualizer. Only `apk` file is needed. +Class coupling is one of the significant code metrics that shows how easy is to change, maintain and test the code. This tool helps to view whole picture of the project. **Table of contents** -* [Theory](#Theory) -* [Project structure](#Project-structure) -* [Compile](#Compile) - * [Requirements](#Requirements) - * [Gradle](#Gradle) - * [5.0 or newer](#50-or-newer) - * [via Wrapper](#via-Wrapper) - * [Ant](#Ant) -* [Run](#Run) * [Usage](#Usage) - * [Fast way](#Fast-way) +* [Compile](#Compile) * [Examples](#Examples) * [Demo](#Demo) -* [Credits](#Credits) * [Contributors](#Contributors) -* [Dependency Injection Graph](#Dependency-Injection-Graph) - -## Theory - -Class coupling is one of the significant code metrics which shows how easy is to change your code. Actually the architecture of microservices is based on the idea that the modules should be low-coupled so you are able to easily replace one module with another one with the same interface. This tool helps to view whole picture of your project. Check yourself! - -## Project structure - -This project consists of the several parts: - -* gui (d3) -* src (apk-dependency-graph) - -###### To get more information please check our [wiki page](https://github.com/alexzaitsev/apk-dependency-graph/wiki). - -## Compile - -### Requirements - -* at least **Java 5** - -Ways to compile `build/jar/apk-dependency-graph.jar`: - -### Gradle - -#### 5.0 or newer - -run `gradle build` - -#### via Wrapper - -run `gradlew build` - -### Ant - -From terminal just move to the parent folder of the project and run `ant` command. Classes will be generated to `build/classes` folder and jar file will appear onto `build/jar` folder. - -## Run - -You need at least **Java 8** to run `apk-dependency-graph` `jar` file. +* [Credits](#Credits) ## Usage -### Fast way - -I've prepared helpful scripts for you. All you need to do is to download and unpack [the latest release](https://github.com/alexzaitsev/apk-dependency-graph/releases) and type the next command in your command line: +Some helpful scripts are prepared for you. All you need to do is to download and unpack [the latest release](https://github.com/alexzaitsev/apk-dependency-graph/releases) and type the next command in your command line: *For Windows*: ```shell -run.bat full\path\to\the\apk\app-release.apk com.example.test true +run.bat full\path\to\the\apk\app-release.apk full\path\to\the\filterset.json ``` -or - -```shell -run.bat full\path\to\the\apk\app-release.apk nofilter false -``` +Where: +* `run.bat` is a path to script in your local repository +* `full\path\to\the\apk\app-release.apk` is a full path to the apk file you want to analize +* `full\path\to\the\filterset.json` is a full path to the filterset file -where `run.bat` is a path to script in your local repository, `full\path\to\the\apk\app-release.apk` is a full path to the apk file you want to analize, `com.example.test` is a filter. **We recommend to use your package name as a filter so you will avoid unnecessary dependencies in your graph. If you don't want to filter just pass `nofilter`.** The last argument defines whether you want to skip inner classes on your graph (_true_ to skip, _false_ otherwise). +The tool is provided with the [default filterset](https://github.com/alexzaitsev/apk-dependency-graph/blob/master/filters/default.json). However, you're highly encouraged to customize it. Read [filter instructions](https://github.com/alexzaitsev/apk-dependency-graph/blob/master/filters/instructions.txt) for the details. *For Unix*: ```shell -./run.sh full/path/to/the/apk/app-release.apk com.example.test true -``` - -or - -```shell -./run.sh full/path/to/the/apk/app-release.apk nofilter false +./run.sh full/path/to/the/apk/app-release.apk full/path/to/the/filterset.json ``` Wait until the command finishes: ```shell Baksmaling classes.dex... +Analyzing dependencies... Success! Now open index.html in your browser. ``` -It will decompile your apk and create `apk-file-name` folder in the same folder where the script is. After this it will analyze the smali code and generate `gui/analyzed.js` file which contains all dependencies. +It will decompile your apk and create `output/apk-file-name` folder in the same folder where the script is. After this it will analyze the smali code and generate `gui/analyzed.js` file which contains all dependencies. **Now open `gui/index.html` in your browser and enjoy!** +## Compile + +At least **Java 8** is needed to compile and run the `jar` file. + +Ways to compile `build/jar/apk-dependency-graph.jar`: + +`>> gradle build` (Gradle 5.0 or newer) +`>> gradlew build` (Gradle Wrapper) +`>> ant` (Ant) + +Classes will be generated to `build/classes` folder and jar file will appear onto `build/jar` folder. + +###### To get more information please check our [wiki page](https://github.com/alexzaitsev/apk-dependency-graph/wiki). + ## Examples Here is the sample of good architecture with low class coupling: @@ -118,25 +76,12 @@ Does your project look like the first or the second picture? :) Watch [demo video](https://www.youtube.com/watch?v=rw501tvT4ko). ---- - -## Credits - -There is the same tool for iOS: -I have used `gui/index.html` of that project. Thanks Paul for the great tool. - ## Contributors -I want to say thank you to all the people who made even tiny pull request. This project is intended to improve current state of Android architecture all over the world so each detail is important. Below you can find a list of people who have found some time to improve this tool: - -* [WarrenFaith](https://github.com/WarrenFaith) -* [victorrattis](https://github.com/victorrattis) -* [SupinePandora43](https://github.com/SupinePandora43) +I want to say thank you to all the people who made even tiny pull request. This project is intended to improve current state of Android architecture all over the world so each detail is important. In the [contributors page](https://github.com/alexzaitsev/apk-dependency-graph/graphs/contributors) you can find a list of people who have found some time to improve this tool. -##### Btw we still need contributors! - -Yes, we really need you man! We always have something to do and have special label for such issues. If you want to add or edit something on this tool - [welcome](https://github.com/alexzaitsev/apk-dependency-graph/issues?q=is%3Aissue+is%3Aopen+label%3A%22contributors+wanted%22)! +## Credits -## Dependency Injection Graph +There is the same tool for iOS: +I have used `gui/index.html` of that project. Thanks Paul for the great tool. -If you're looking for an Android Studio plugin that allows to display graph of dependency injections - please check out [this repository](https://github.com/kaygisiz/Dependency-Injection-Graph). It's based on current project and available in [Jetbrains repository](https://plugins.jetbrains.com/plugin/10107-dependency-injection-graph). diff --git a/build.gradle b/build.gradle index 140a49c..c412164 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,31 @@ -ant.importBuild 'build.xml' - -task build { - dependsOn resolve - dependsOn jar - // https://stackoverflow.com/a/32909428/9765252 - tasks.findByName('jar').mustRunAfter 'resolve' -} \ No newline at end of file +plugins { + id 'java' + id 'com.adarshr.test-logger' version '1.7.0' // https://github.com/radarsh/gradle-test-logger-plugin +} +repositories { + mavenCentral() +} +jar { + manifest { + attributes( + 'Main-Class': 'com.alex_zaitsev.adg.Main' + ) + } +} +testlogger { + theme 'standard-parallel' // theme + slowThreshold 0 // show time +} +clean { + delete = [ // delete anything excluding compiled jar + 'build/classes', + 'build/generated', + 'build/tmp', + 'build/test-results', + 'build/reports'] +} +dependencies { + implementation 'org.smali:baksmali:2.2.5' // runtime dependency + testImplementation 'org.hamcrest:hamcrest:2.1' // test dependency + testImplementation 'junit:junit:4.12' // test dependency +} diff --git a/build.xml b/build.xml index caff1ca..8734130 100644 --- a/build.xml +++ b/build.xml @@ -1,12 +1,14 @@ - - - - - - + + + + + + + + @@ -17,11 +19,33 @@ - + + + + + + + + + + + + + + + + + + + @@ -35,13 +59,9 @@ - - - - - + @@ -71,11 +91,17 @@ - + + + + + + + diff --git a/filters/default.json b/filters/default.json new file mode 100644 index 0000000..9a903c5 --- /dev/null +++ b/filters/default.json @@ -0,0 +1,5 @@ +{ + "package-name": "", + "show-inner-classes": false, + "ignored-classes": [".*Dagger.*", ".*Inject.*", ".*ViewBinding$", ".*Factory$", ".*_.*", "^R$", "^R\\$.*"] +} \ No newline at end of file diff --git a/filters/instructions.txt b/filters/instructions.txt new file mode 100644 index 0000000..a4007fb --- /dev/null +++ b/filters/instructions.txt @@ -0,0 +1,12 @@ +'default.json' is a default filterset that is shipped with the releases. + +It consists of the next filters: +package-name - Java package to filter by, e.g. "com.mysite.myandroidapp". Empty by default. +show-inner-classes - If true it will contain inner class processing (the ones creating ClassName$InnerClass files). False by default. +ignored-classes - The list of regexps that will be applied to class names. + If class name matches any of provided regexps - it will be ignored. + By default it filters Dagger, ButterKnife, R and some generated classes. + +You may extend this file or create a set of your own filter files for each project. + +Read Usage part of Readme for the details. diff --git a/ivy.xml b/ivy.xml index 23e67d3..5df0056 100644 --- a/ivy.xml +++ b/ivy.xml @@ -3,5 +3,12 @@ + + + + + + + diff --git a/release.sh b/release.sh index 8cf5c98..2b72f1f 100644 --- a/release.sh +++ b/release.sh @@ -5,4 +5,4 @@ if [ $# -ne 1 ]; then fi gradle build echo "var dependencies = {links:[{\"source\":\"Class A\",\"dest\":\"Class B\"},{\"source\":\"Class C\",\"dest\":\"Class B\"},]};" > gui/analyzed.js -zip -u -x .DS_Store -r "apk-dependency-graph-scripts-$1.zip" build/jar/apk-dependency-graph.jar build.xml gui/* lib/* run.bat run.sh +zip -u -x .DS_Store -r "apk-dependency-graph-scripts-$1.zip" build/jar/apk-dependency-graph.jar build.xml gui/* lib/* filters/default.json filters/instructions.txt run.bat run.sh diff --git a/run.bat b/run.bat index 62c73d8..eeb4040 100644 --- a/run.bat +++ b/run.bat @@ -5,14 +5,12 @@ set argCount=0 for %%x in (%*) do ( set /A argCount+=1 ) -if not "%~3"=="" if "%~4"=="" goto main +if not "%~2"=="" if "%~3"=="" goto main echo This script requires the next parameters: echo - absolute path to apk file - echo - filter ^(can be a package name or 'nofilter' string^) - echo - true or false ^(where true means that you want to see inner classes on your graph^) + echo - absolute path to the filters file echo Examples: - echo %~nx0 full\path\to\the\apk\app-release.apk com.example.test true - echo %~nx0 full\path\to\the\apk\app-release.apk nofilter false + echo %~nx0 full\path\to\the\apk\app-release.apk full\path\to\the\filters.json exit /b :main @@ -26,4 +24,4 @@ For %%A in ("%filename%") do ( Set outPath=%~dp0\output\%Name:~0,-4% Set jsonPath=%~dp0\gui\analyzed.js -java -jar %~dp0\build\jar\apk-dependency-graph.jar -i %outPath% -o %jsonPath% -f %2 -d %3 -a %1 +java -jar %~dp0\build\jar\apk-dependency-graph.jar -i %outPath% -o %jsonPath% -a %1 -f %2 diff --git a/run.sh b/run.sh index 869a679..92f8634 100755 --- a/run.sh +++ b/run.sh @@ -1,12 +1,10 @@ #!/bin/bash -if [ $# -lt 3 ]; then +if [ $# -lt 2 ]; then echo "This script requires the next parameters:"; echo "- absolute path to apk file"; - echo "- filter (can be a package name or 'nofilter' string)"; - echo "- true or false (where true means that you want to see inner classes on your graph)"; - echo "Examples:"; - echo "./run.sh full/path/to/the/apk/app-release.apk com.example.test true"; - echo "./run.sh full/path/to/the/apk/app-release.apk nofilter false"; + echo "- absolute path to the filters file"; + echo "Example:"; + echo "./run.sh full/path/to/the/apk/app-release.apk full/path/to/the/filters.json"; exit 1; fi fileName="$1" @@ -17,4 +15,4 @@ dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" outPath=${dir}"/output/"${xpref} jsonPath=${dir}"/gui/analyzed.js" -eval "java -jar ${dir}'/build/jar/apk-dependency-graph.jar' -i ${outPath} -o ${jsonPath} -f $2 -d $3 -a $1" +eval "java -jar ${dir}'/build/jar/apk-dependency-graph.jar' -i ${outPath} -o ${jsonPath} -a $1 -f $2" diff --git a/src/code/CodeUtils.java b/src/code/CodeUtils.java deleted file mode 100644 index ef73f58..0000000 --- a/src/code/CodeUtils.java +++ /dev/null @@ -1,41 +0,0 @@ -package code; - -import code.util.StringUtils; - -public class CodeUtils { - - public static boolean isClassR(String className) { - return className != null && className.equals("R") || className.startsWith("R$"); - } - - public static boolean isClassGenerated(String className) { - return className != null && className.contains("$$"); - } - - public static boolean isClassInner(String className) { - return className != null && className.contains("$") && !isClassAnonymous(className) && !isClassGenerated(className); - } - - public static String getOuterClass(String className) { - return className.substring(0, className.lastIndexOf("$")); - } - - public static boolean isClassAnonymous(String className) { - return className != null && className.contains("$") - && StringUtils.isNumber(className.substring(className.lastIndexOf("$") + 1, className.length())); - } - - public static String getAnonymousNearestOuter(String className) { - String[] classes = className.split("\\$"); - for (int i = 0; i < classes.length; i++) { - if (StringUtils.isNumber(classes[i])) { - String anonHolder = ""; - for (int j = 0; j < i; j++) { - anonHolder += classes[j] + (j == i - 1 ? "" : "$"); - } - return anonHolder; - } - } - return null; - } -} diff --git a/src/code/Main.java b/src/code/Main.java deleted file mode 100644 index 34af0c3..0000000 --- a/src/code/Main.java +++ /dev/null @@ -1,35 +0,0 @@ -package code; - -import code.decode.ApkSmaliDecoderController; -import code.io.ArgumentReader; -import code.io.Arguments; -import code.io.Writer; -import code.util.FileUtils; - -import java.io.File; - -public class Main { - - public static void main(String[] args) { - Arguments arguments = new ArgumentReader(args).read(); - if (arguments == null) { - return; - } - - // Delete the output directory for a better decoding result. - if (FileUtils.deleteDir(arguments.getProjectPath())) { - System.out.println("The output directory was deleted!"); - } - - // Decode the APK file for smali code in the output directory. - ApkSmaliDecoderController.decode( - arguments.getApkFilePath(), arguments.getProjectPath()); - - File resultFile = new File(arguments.getResultPath()); - SmaliAnalyzer analyzer = new SmaliAnalyzer(arguments); - if (analyzer.run()) { - new Writer(resultFile).write(analyzer.getDependencies()); - System.out.println("Success! Now open index.html in your browser."); - } - } -} \ No newline at end of file diff --git a/src/code/io/ArgumentReader.java b/src/code/io/ArgumentReader.java deleted file mode 100644 index af9826b..0000000 --- a/src/code/io/ArgumentReader.java +++ /dev/null @@ -1,62 +0,0 @@ -package code.io; - -import java.io.File; - -public class ArgumentReader { - - private static final String USAGE_STRING = "Usage:\n" + - "-i path : path to the decompiled project\n" + - "-o path : path to the result js file\n" + - "-f filter : java package to filter by (to show all dependencies pass '-f nofilter')\n" + - "-d boolean : if true it will contain inner class processing (the ones creating ClassName$InnerClass files)\n" + - "-a path : path to the apk file"; - - private String[] args; - - public ArgumentReader(String[] args) { - this.args = args; - } - - public Arguments read() { - String projectPath = null, resultPath = null, filter = null; - String apkPath = null; - boolean withInnerClasses = false; - for (int i = 0; i < args.length; i++) { - if (i < args.length - 1) { - if (args[i].equals("-i")) { - projectPath = args[i + 1]; - } else if (args[i].equals("-o")) { - resultPath = args[i + 1]; - } else if (args[i].equals("-f")) { - filter = args[i + 1]; - } else if (args[i].equals("-d")) { - withInnerClasses = Boolean.valueOf(args[i + 1]); - } else if (args[i].equals("-a")) { - apkPath = args[i + 1]; - } - } - } - if (projectPath == null || resultPath == null || filter == null || - apkPath == null) { - System.err.println("Arguments are incorrect!"); - System.err.println(USAGE_STRING); - return null; - } - if (filter.equals("nofilter")) { - filter = null; - System.out.println("Warning! Processing without filter."); - } - if (!withInnerClasses) { - System.out.println("Warning! Processing without inner classes."); - } - - File apkFile = new File(apkPath); - if (!apkFile.exists()) { - System.out.println(apkFile + " is not found!"); - return null; - } - - return new Arguments( - apkPath, projectPath, resultPath, filter, withInnerClasses); - } -} diff --git a/src/main/java/com/alex_zaitsev/adg/FilterProvider.java b/src/main/java/com/alex_zaitsev/adg/FilterProvider.java new file mode 100644 index 0000000..fe70b5c --- /dev/null +++ b/src/main/java/com/alex_zaitsev/adg/FilterProvider.java @@ -0,0 +1,37 @@ +package com.alex_zaitsev.adg; + +import com.alex_zaitsev.adg.io.*; +import com.alex_zaitsev.adg.filter.*; + +import java.io.File; +import java.util.regex.Matcher; + +public class FilterProvider { + + private Filters inputFilters; + + public FilterProvider(Filters inputFilters) { + this.inputFilters = inputFilters; + } + + public Filter makePathFilter() { + if (inputFilters.getPackageName() == null || inputFilters.getPackageName().isEmpty()) { + return null; + } + + String replacement = Matcher.quoteReplacement(File.separator); + replacement = Matcher.quoteReplacement(replacement); + String packageNameAsPath = inputFilters.getPackageName().replaceAll("\\.", replacement); + String packageNameRegex = ".*" + packageNameAsPath + ".*"; + RegexFilter filter = new RegexFilter(packageNameRegex); + + return filter; + } + + public Filter makeClassFilter() { + String[] ignoredClasses = inputFilters.getIgnoredClasses(); + InverseRegexFilter ignoredClassesFilter = new InverseRegexFilter(ignoredClasses); + + return ignoredClassesFilter; + } +} \ No newline at end of file diff --git a/src/main/java/com/alex_zaitsev/adg/Main.java b/src/main/java/com/alex_zaitsev/adg/Main.java new file mode 100644 index 0000000..bcc3eb9 --- /dev/null +++ b/src/main/java/com/alex_zaitsev/adg/Main.java @@ -0,0 +1,48 @@ +package com.alex_zaitsev.adg; + +import com.alex_zaitsev.adg.FilterProvider; +import com.alex_zaitsev.adg.decode.ApkSmaliDecoderController; +import com.alex_zaitsev.adg.io.ArgumentReader; +import com.alex_zaitsev.adg.io.Arguments; +import com.alex_zaitsev.adg.io.FiltersReader; +import com.alex_zaitsev.adg.io.Filters; +import com.alex_zaitsev.adg.io.Writer; +import com.alex_zaitsev.adg.util.FileUtils; +import com.alex_zaitsev.adg.filter.Filter; + +import java.io.File; + +public class Main { + + public static void main(String[] args) { + Arguments arguments = new ArgumentReader(args).read(); + if (arguments == null) { + System.err.println("Arguments cannot be null!"); + return; + } + Filters filters = arguments.getFiltersPath() == null ? null : + new FiltersReader(arguments.getFiltersPath()).read(); + + // Delete the output directory for a better decoding result. + if (FileUtils.deleteDir(arguments.getProjectPath())) { + System.out.println("The output directory was deleted!"); + } + + // Decode the APK file for smali code in the output directory. + ApkSmaliDecoderController.decode( + arguments.getApkFilePath(), arguments.getProjectPath()); + + // Analyze the decoded files and create the result file. + FilterProvider filterProvider = new FilterProvider(filters); + Filter pathFilter = filters == null ? null : filterProvider.makePathFilter(); + Filter classFilter = filters == null ? null : filterProvider.makeClassFilter(); + SmaliAnalyzer analyzer = new SmaliAnalyzer(arguments, filters, + pathFilter, classFilter); + + if (analyzer.run()) { + File resultFile = new File(arguments.getResultPath()); + new Writer(resultFile).write(analyzer.getDependencies()); + System.out.println("Success! Now open index.html in your browser."); + } + } +} \ No newline at end of file diff --git a/src/code/SmaliAnalyzer.java b/src/main/java/com/alex_zaitsev/adg/SmaliAnalyzer.java similarity index 58% rename from src/code/SmaliAnalyzer.java rename to src/main/java/com/alex_zaitsev/adg/SmaliAnalyzer.java index f8b1210..10562e6 100644 --- a/src/code/SmaliAnalyzer.java +++ b/src/main/java/com/alex_zaitsev/adg/SmaliAnalyzer.java @@ -1,4 +1,4 @@ -package code; +package com.alex_zaitsev.adg; import java.io.BufferedReader; import java.io.File; @@ -9,93 +9,102 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import code.io.Arguments; +import com.alex_zaitsev.adg.io.Arguments; +import com.alex_zaitsev.adg.io.Filters; +import com.alex_zaitsev.adg.filter.Filter; + +import static com.alex_zaitsev.adg.util.CodeUtils.isClassGenerated; +import static com.alex_zaitsev.adg.util.CodeUtils.isClassInner; +import static com.alex_zaitsev.adg.util.CodeUtils.getOuterClass; +import static com.alex_zaitsev.adg.util.CodeUtils.isClassAnonymous; +import static com.alex_zaitsev.adg.util.CodeUtils.getAnonymousNearestOuter; +import static com.alex_zaitsev.adg.util.CodeUtils.getEndGenericIndex; +import static com.alex_zaitsev.adg.util.CodeUtils.getClassSimpleName; +import static com.alex_zaitsev.adg.util.CodeUtils.isInstantRunEnabled; +import static com.alex_zaitsev.adg.util.CodeUtils.isSmaliFile; public class SmaliAnalyzer { private Arguments arguments; - private String filterAsPath; - - public SmaliAnalyzer(Arguments arguments) { + private Filters filters; + private Filter pathFilter; + private Filter classFilter; + + public SmaliAnalyzer(Arguments arguments, + Filters filters, + Filter pathFilter, + Filter classFilter) { this.arguments = arguments; + this.filters = filters; + this.pathFilter = pathFilter; + this.classFilter = classFilter; } private Map> dependencies = new HashMap<>(); public Map> getDependencies() { - if (arguments.withInnerClasses()) { + if (filters == null || filters.isProcessingInner()) { return dependencies; } return getFilteredDependencies(); } public boolean run() { - String filter = arguments.getFilter(); - if (filter == null) { - System.err.println("Please check your filter!"); - return false; - } - - String replacement = Matcher.quoteReplacement(File.separator); - String searchString = Pattern.quote("."); - filterAsPath = filter.replaceAll(searchString, replacement); - File projectFolder = getProjectFolder(); - if (projectFolder.exists()) { - traverseSmaliCode(projectFolder); - return true; - } else if (isInstantRunEnabled()){ - System.err.println("Enabled Instant Run feature detected. We cannot decompile it. Please, disable Instant Run and rebuild your app."); - } else { - System.err.println("Smali folder cannot be absent!"); - } - return false; - } - - private File getProjectFolder() { - return new File(arguments.getProjectPath()); - } - - private boolean isInstantRunEnabled() { - File unknownFolder = new File(arguments.getProjectPath() + File.separator + "unknown"); - if (unknownFolder.exists()) { - for (File file : unknownFolder.listFiles()) { - if (file.getName().equals("instant-run.zip")) { - return true; - } + System.out.println("Analyzing dependencies..."); + + File projectDir = new File(arguments.getProjectPath()); + if (projectDir.exists()) { + if (isInstantRunEnabled(arguments.getProjectPath())) { + System.err.println("Enabled Instant Run feature detected. " + + "We cannot decompile it. Please, disable Instant Run and rebuild your app."); + } else { + traverseSmaliCodeDir(projectDir); + return true; } - + } else { + System.err.println(projectDir + " does not exist!"); } return false; } - private void traverseSmaliCode(File folder) { - File[] listOfFiles = folder.listFiles(); + private void traverseSmaliCodeDir(File dir) { + File[] listOfFiles = dir.listFiles(); for (int i = 0; i < listOfFiles.length; i++) { File currentFile = listOfFiles[i]; - if (currentFile.isFile()) { - if (currentFile.getName().endsWith(".smali") && currentFile.getAbsolutePath().contains(filterAsPath)) { + if (isSmaliFile(currentFile)) { + if (isPathFilterOk(currentFile)) { processSmaliFile(currentFile); } } else if (currentFile.isDirectory()) { - traverseSmaliCode(currentFile); + traverseSmaliCodeDir(currentFile); } } } + private boolean isPathFilterOk(File file) { + return isPathFilterOk(file.getAbsolutePath()); + } + + private boolean isPathFilterOk(String filePath) { + return pathFilter == null || pathFilter.filter(filePath); + } + + private boolean isClassFilterOk(String className) { + return classFilter == null || classFilter.filter(className); + } + private void processSmaliFile(File file) { try (BufferedReader br = new BufferedReader(new FileReader(file))) { String fileName = file.getName().substring(0, file.getName().lastIndexOf(".")); - if (CodeUtils.isClassR(fileName)) { - return; + if (isClassAnonymous(fileName)) { + fileName = getAnonymousNearestOuter(fileName); } - - if (CodeUtils.isClassAnonymous(fileName)) { - fileName = CodeUtils.getAnonymousNearestOuter(fileName); + + if (!isClassFilterOk(fileName)) { + return; } Set classNames = new HashSet<>(); @@ -109,20 +118,21 @@ private void processSmaliFile(File file) { // filtering for (String fullClassName : classNames) { - if (fullClassName != null && isFilterOk(fullClassName)) { + if (fullClassName != null && isPathFilterOk(fullClassName)) { String simpleClassName = getClassSimpleName(fullClassName); - if (isClassOk(simpleClassName, fileName)) { + if (isClassFilterOk(simpleClassName) && isClassOk(simpleClassName, fileName)) { dependencyNames.add(simpleClassName); } } } } catch (Exception e) { + System.err.println("Error '" + e.getMessage() + "' occured."); } } // inner/nested class always depends on the outer class - if (CodeUtils.isClassInner(fileName)) { - dependencyNames.add(CodeUtils.getOuterClass(fileName)); + if (isClassInner(fileName)) { + dependencyNames.add(getOuterClass(fileName)); } if (!dependencyNames.isEmpty()) { @@ -134,27 +144,17 @@ private void processSmaliFile(File file) { System.err.println("Cannot read " + file.getAbsolutePath()); } } - - private String getClassSimpleName(String fullClassName) { - String simpleClassName = fullClassName.substring(fullClassName.lastIndexOf("/") + 1, - fullClassName.length()); - int startGenericIndex = simpleClassName.indexOf("<"); - if (startGenericIndex != -1) { - simpleClassName = simpleClassName.substring(0, startGenericIndex); - } - return simpleClassName; - } /** * The last filter. Do not show anonymous classes (their dependencies belongs to outer class), - * generated classes, avoid circular dependencies, do not show generated R class + * generated classes, avoid circular dependencies * @param simpleClassName class name to inspect * @param fileName full class name * @return true if class is good with these conditions */ private boolean isClassOk(String simpleClassName, String fileName) { - return !CodeUtils.isClassAnonymous(simpleClassName) && !CodeUtils.isClassGenerated(simpleClassName) - && !fileName.equals(simpleClassName) && !CodeUtils.isClassR(simpleClassName); + return !isClassAnonymous(simpleClassName) && !isClassGenerated(simpleClassName) + && !fileName.equals(simpleClassName); } private void parseAndAddClassNames(Set classNames, String line) { @@ -186,21 +186,7 @@ private void parseAndAddClassNames(Set classNames, String line) { classNames.add(className); } - } - - private int getEndGenericIndex(String line, int startGenericIndex) { - int endIndex = line.indexOf(">", startGenericIndex); - for (int i = endIndex + 2; i < line.length(); i += 2) { - if (line.charAt(i) == '>') { - endIndex = i; - } - } - return endIndex; - } - - private boolean isFilterOk(String className) { - return arguments.getFilter() == null || className.startsWith(arguments.getFilter().replaceAll("\\.", "/")); - } + } private void addDependencies(String className, Set dependenciesList) { Set depList = dependencies.get(className); diff --git a/src/code/decode/ApkSmaliDecoder.java b/src/main/java/com/alex_zaitsev/adg/decode/ApkSmaliDecoder.java similarity index 97% rename from src/code/decode/ApkSmaliDecoder.java rename to src/main/java/com/alex_zaitsev/adg/decode/ApkSmaliDecoder.java index e6fb629..30907a4 100644 --- a/src/code/decode/ApkSmaliDecoder.java +++ b/src/main/java/com/alex_zaitsev/adg/decode/ApkSmaliDecoder.java @@ -1,6 +1,6 @@ -package code.decode; +package com.alex_zaitsev.adg.decode; -import code.util.ZipFileUtils; +import com.alex_zaitsev.adg.util.ZipFileUtils; import org.jf.baksmali.Baksmali; import org.jf.baksmali.BaksmaliOptions; diff --git a/src/code/decode/ApkSmaliDecoderController.java b/src/main/java/com/alex_zaitsev/adg/decode/ApkSmaliDecoderController.java similarity index 93% rename from src/code/decode/ApkSmaliDecoderController.java rename to src/main/java/com/alex_zaitsev/adg/decode/ApkSmaliDecoderController.java index f95be1b..e4593d7 100644 --- a/src/code/decode/ApkSmaliDecoderController.java +++ b/src/main/java/com/alex_zaitsev/adg/decode/ApkSmaliDecoderController.java @@ -1,4 +1,4 @@ -package code.decode; +package com.alex_zaitsev.adg.decode; import java.io.IOException; diff --git a/src/main/java/com/alex_zaitsev/adg/filter/AndFilter.java b/src/main/java/com/alex_zaitsev/adg/filter/AndFilter.java new file mode 100644 index 0000000..c19cd4b --- /dev/null +++ b/src/main/java/com/alex_zaitsev/adg/filter/AndFilter.java @@ -0,0 +1,46 @@ +package com.alex_zaitsev.adg.filter; + +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; + +/** + * Class that holds filters chain + */ +public class AndFilter extends Filter { + + private List> filters; + + public AndFilter(Filter... filters) { + this.filters = new ArrayList<>(Arrays.asList(filters)); + } + + public void addFilter(Filter filter) { + filters.add(filter); + } + + /** + * Filters the given object + * + * @return true if ALL filters are satisfied, false otherwise + */ + public boolean filter(T obj) { + for (Filter filter: filters) { + if (!filter.filter(obj)) { + return false; + } + } + return true; + } + + public String toString() { + StringBuilder builder = new StringBuilder(getClass().getSimpleName()); + builder.append("["); + for (int i = 0; i < filters.size(); i++) { + builder.append(filters.get(i).toString()); + if (i != filters.size() - 1) builder.append(", "); + } + builder.append("]"); + return builder.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/alex_zaitsev/adg/filter/Filter.java b/src/main/java/com/alex_zaitsev/adg/filter/Filter.java new file mode 100644 index 0000000..1073104 --- /dev/null +++ b/src/main/java/com/alex_zaitsev/adg/filter/Filter.java @@ -0,0 +1,14 @@ +package com.alex_zaitsev.adg.filter; + +/** + * Base filter class in the hierarchy + */ +public abstract class Filter { + + /** + * Filters the object + * + * @return true if object satisfies conditions, false otherwise + */ + public abstract boolean filter(T obj); +} \ No newline at end of file diff --git a/src/main/java/com/alex_zaitsev/adg/filter/InverseRegexFilter.java b/src/main/java/com/alex_zaitsev/adg/filter/InverseRegexFilter.java new file mode 100644 index 0000000..1f80681 --- /dev/null +++ b/src/main/java/com/alex_zaitsev/adg/filter/InverseRegexFilter.java @@ -0,0 +1,19 @@ +package com.alex_zaitsev.adg.filter; + +public class InverseRegexFilter extends RegexFilter { + + public InverseRegexFilter(String regex) { + super(regex); + } + + public InverseRegexFilter(String[] regex) { + super(regex); + } + + /** + * @return true if String doesn't match the given regex, false otherwise + */ + public boolean filter(String obj) { + return !pattern.matcher(obj).matches(); + } +} \ No newline at end of file diff --git a/src/main/java/com/alex_zaitsev/adg/filter/RegexFilter.java b/src/main/java/com/alex_zaitsev/adg/filter/RegexFilter.java new file mode 100644 index 0000000..a9391f4 --- /dev/null +++ b/src/main/java/com/alex_zaitsev/adg/filter/RegexFilter.java @@ -0,0 +1,29 @@ +package com.alex_zaitsev.adg.filter; + +import java.util.regex.Pattern; + +public class RegexFilter extends Filter { + + protected Pattern pattern; + + public RegexFilter(String regex) { + this.pattern = Pattern.compile(regex); + } + + public RegexFilter(String[] regex) { + this(String.join("|", regex)); + } + + /** + * @return true if String matches the given regex, false otherwise + */ + public boolean filter(String obj) { + return pattern.matcher(obj).matches(); + } + + public String toString() { + StringBuilder builder = new StringBuilder(getClass().getSimpleName()); + builder.append("{").append(pattern.toString()).append("}"); + return builder.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/alex_zaitsev/adg/io/ArgumentReader.java b/src/main/java/com/alex_zaitsev/adg/io/ArgumentReader.java new file mode 100644 index 0000000..e0e6901 --- /dev/null +++ b/src/main/java/com/alex_zaitsev/adg/io/ArgumentReader.java @@ -0,0 +1,66 @@ +package com.alex_zaitsev.adg.io; + +import java.io.File; + +public class ArgumentReader { + + private static final String ARG_PROJ = "-i"; + private static final String ARG_RES_JS = "-o"; + private static final String ARG_APK = "-a"; + private static final String ARG_FILTER = "-f"; + + private static final String USAGE_STRING = "Usage:\n" + + ARG_PROJ + " path : path to the decompiled project\n" + + ARG_RES_JS + " path : path to the result js file\n" + + ARG_APK + " path : path to the apk file\n" + + ARG_FILTER + " filters : path to the your_filterset.json file"; + + private String[] args; + + public ArgumentReader(String[] args) { + this.args = args; + } + + public Arguments read() { + String projectPath = null, resultPath = null, apkPath = null, filtersPath = null; + + for (int i = 0; i < args.length; i++) { + if (i < args.length - 1) { + if (args[i].equals(ARG_PROJ)) { + projectPath = args[i + 1]; + } else if (args[i].equals(ARG_RES_JS)) { + resultPath = args[i + 1]; + } else if (args[i].equals(ARG_APK)) { + apkPath = args[i + 1]; + } else if (args[i].equals(ARG_FILTER)) { + filtersPath = args[i + 1]; + } + } + } + if (projectPath == null || resultPath == null || apkPath == null) { + System.err.println(ARG_PROJ + ", " + ARG_RES_JS + " and " + ARG_APK + " must be provided!"); + System.err.println(USAGE_STRING); + return null; + } + + if (!checkFiles(new String[] {apkPath})) { + return null; + } + if (filtersPath != null && !checkFiles(new String[] {filtersPath})) { + return null; + } + + return new Arguments(apkPath, projectPath, resultPath, filtersPath); + } + + private boolean checkFiles(String[] files) { + for (String fileName: files) { + File file = new File(fileName); + if (!file.exists()) { + System.err.println(file + " is not found!"); + return false; + } + } + return true; + } +} diff --git a/src/code/io/Arguments.java b/src/main/java/com/alex_zaitsev/adg/io/Arguments.java similarity index 57% rename from src/code/io/Arguments.java rename to src/main/java/com/alex_zaitsev/adg/io/Arguments.java index 8f8ae11..e625812 100644 --- a/src/code/io/Arguments.java +++ b/src/main/java/com/alex_zaitsev/adg/io/Arguments.java @@ -1,46 +1,49 @@ -package code.io; +package com.alex_zaitsev.adg.io; public class Arguments { + private String apkFilePath; private String projectPath; private String resultPath; - private String filter; - private boolean withInnerClasses; + private String filtersPath; public Arguments(String apkPath, String projectPath, String resultPath, - String filter, boolean withInnerClasses) { - super(); + String filtersPath) { this.apkFilePath = apkPath; this.projectPath = projectPath; this.resultPath = resultPath; - this.filter = filter; - this.withInnerClasses = withInnerClasses; + this.filtersPath = filtersPath; } + public String getProjectPath() { return projectPath; } + public void setProjectPath(String projectPath) { this.projectPath = projectPath; } + public String getResultPath() { return resultPath; } + public void setResultPath(String resultPath) { this.resultPath = resultPath; } - public String getFilter() { - return filter; - } - public void setFilter(String filter) { - this.filter = filter; + + public String getApkFilePath() { + return this.apkFilePath; } - public boolean withInnerClasses() { - return withInnerClasses; + + public void setApkFilePath(String apkFilePath) { + this.apkFilePath = apkFilePath; } - public void setWithInnerClasses(boolean withInnerClasses) { - this.withInnerClasses = withInnerClasses; + + public String getFiltersPath() { + return filtersPath; } - public String getApkFilePath() { - return this.apkFilePath; + + public void setFiltersPath(String filtersPath) { + this.filtersPath = filtersPath; } } diff --git a/src/main/java/com/alex_zaitsev/adg/io/Filters.java b/src/main/java/com/alex_zaitsev/adg/io/Filters.java new file mode 100644 index 0000000..367017d --- /dev/null +++ b/src/main/java/com/alex_zaitsev/adg/io/Filters.java @@ -0,0 +1,41 @@ +package com.alex_zaitsev.adg.io; + +public class Filters { + + public static final boolean DEFAULT_PROCESS_INNER = false; + + private String packageName = null; + private boolean processingInner = DEFAULT_PROCESS_INNER; + private String[] ignoredClasses = null; + + public Filters(String packageName, boolean processingInner, + String[] ignoredClasses) { + this.packageName = packageName; + this.processingInner = processingInner; + this.ignoredClasses = ignoredClasses; + } + + public String getPackageName() { + return packageName; + } + + public void setPackageName(String packageName) { + this.packageName = packageName; + } + + public boolean isProcessingInner() { + return processingInner; + } + + public void setProcessingInner(boolean isProcessingInner) { + this.processingInner = isProcessingInner; + } + + public String[] getIgnoredClasses() { + return ignoredClasses; + } + + public void setIgnoredClasses(String[] ignoredClasses) { + this.ignoredClasses = ignoredClasses; + } +} \ No newline at end of file diff --git a/src/main/java/com/alex_zaitsev/adg/io/FiltersReader.java b/src/main/java/com/alex_zaitsev/adg/io/FiltersReader.java new file mode 100644 index 0000000..08defae --- /dev/null +++ b/src/main/java/com/alex_zaitsev/adg/io/FiltersReader.java @@ -0,0 +1,61 @@ +package com.alex_zaitsev.adg.io; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class FiltersReader { + + private static final String FILTER_PACKAGE_NAME = "package-name"; + private static final String FILTER_SHOW_INNER = "show-inner-classes"; + private static final String FILTER_IGNORED_CLASSES = "ignored-classes"; + + private String filtersFilePath; + + public FiltersReader(String filtersFilePath) { + this.filtersFilePath = filtersFilePath; + } + + /** + * Parses the your_filterset.json file and produces Filters object + */ + public Filters read() { + String packageName = null; + boolean showInnerClasses = Filters.DEFAULT_PROCESS_INNER; + String[] ignoredClassesArr = null; + + try { + String content = new String(Files.readAllBytes(Paths.get(filtersFilePath))); + String mainObject = content.replace("{", "").replace("}", "").trim(); + String[] rawParams = mainObject.split(","); + + for (String rawParam: rawParams) { + if (rawParam.contains(FILTER_PACKAGE_NAME)) { + packageName = rawParam.trim().split(":")[1].trim().replace("\"", ""); + } + if (rawParam.contains(FILTER_SHOW_INNER)) { + showInnerClasses = Boolean.valueOf(rawParam.trim().split(":")[1].trim().replace("\"", "")); + } + } + String ignoredClasses = mainObject.substring(mainObject.indexOf('[') + 1, mainObject.lastIndexOf(']')); + ignoredClassesArr = ignoredClasses.split(","); + for (int i = 0; i < ignoredClassesArr.length; i++) { + ignoredClassesArr[i] = ignoredClassesArr[i].replace("\"", "").trim(); + } + } catch (Exception e) { + System.err.println("An error happened during " + filtersFilePath + " processing!"); + e.printStackTrace(); + return null; + } + + if (packageName == null || packageName.isEmpty()) { + packageName = null; + System.out.println("Warning! Processing without package filter."); + } + if (showInnerClasses) { + System.out.println("Warning! Processing including inner classes."); + } + + return new Filters(packageName, showInnerClasses, ignoredClassesArr); + } +} \ No newline at end of file diff --git a/src/code/io/Writer.java b/src/main/java/com/alex_zaitsev/adg/io/Writer.java similarity index 96% rename from src/code/io/Writer.java rename to src/main/java/com/alex_zaitsev/adg/io/Writer.java index eb3fc03..ff05284 100644 --- a/src/code/io/Writer.java +++ b/src/main/java/com/alex_zaitsev/adg/io/Writer.java @@ -1,4 +1,4 @@ -package code.io; +package com.alex_zaitsev.adg.io; import java.io.BufferedWriter; import java.io.File; diff --git a/src/main/java/com/alex_zaitsev/adg/util/CodeUtils.java b/src/main/java/com/alex_zaitsev/adg/util/CodeUtils.java new file mode 100644 index 0000000..2475521 --- /dev/null +++ b/src/main/java/com/alex_zaitsev/adg/util/CodeUtils.java @@ -0,0 +1,73 @@ +package com.alex_zaitsev.adg.util; + +import java.io.File; + +public class CodeUtils { + + public static boolean isClassGenerated(String className) { + return className != null && className.contains("$$"); + } + + public static boolean isClassInner(String className) { + return className != null && className.contains("$") && !isClassAnonymous(className) && !isClassGenerated(className); + } + + public static String getOuterClass(String className) { + return className.substring(0, className.lastIndexOf("$")); + } + + public static boolean isClassAnonymous(String className) { + return className != null && className.contains("$") + && StringUtils.isNumber(className.substring(className.lastIndexOf("$") + 1, className.length())); + } + + public static String getAnonymousNearestOuter(String className) { + String[] classes = className.split("\\$"); + for (int i = 0; i < classes.length; i++) { + if (StringUtils.isNumber(classes[i])) { + String anonHolder = ""; + for (int j = 0; j < i; j++) { + anonHolder += classes[j] + (j == i - 1 ? "" : "$"); + } + return anonHolder; + } + } + return null; + } + + public static int getEndGenericIndex(String line, int startGenericIndex) { + int endIndex = line.indexOf(">", startGenericIndex); + for (int i = endIndex + 2; i < line.length(); i += 2) { + if (line.charAt(i) == '>') { + endIndex = i; + } + } + return endIndex; + } + + public static String getClassSimpleName(String fullClassName) { + String simpleClassName = fullClassName.substring(fullClassName.lastIndexOf("/") + 1, + fullClassName.length()); + int startGenericIndex = simpleClassName.indexOf("<"); + if (startGenericIndex != -1) { + simpleClassName = simpleClassName.substring(0, startGenericIndex); + } + return simpleClassName; + } + + public static boolean isInstantRunEnabled(String projectPath) { + File unknownDir = new File(projectPath, "unknown"); + if (unknownDir.exists() && unknownDir.isDirectory()) { + for (File file : unknownDir.listFiles()) { + if (file.getName().equals("instant-run.zip")) { + return true; + } + } + } + return false; + } + + public static boolean isSmaliFile(File file) { + return file.isFile() && file.getName().endsWith(".smali"); + } +} diff --git a/src/code/util/FileUtils.java b/src/main/java/com/alex_zaitsev/adg/util/FileUtils.java similarity index 93% rename from src/code/util/FileUtils.java rename to src/main/java/com/alex_zaitsev/adg/util/FileUtils.java index e96bafd..6588590 100644 --- a/src/code/util/FileUtils.java +++ b/src/main/java/com/alex_zaitsev/adg/util/FileUtils.java @@ -1,4 +1,4 @@ -package code.util; +package com.alex_zaitsev.adg.util; import java.io.File; diff --git a/src/code/util/StringUtils.java b/src/main/java/com/alex_zaitsev/adg/util/StringUtils.java similarity index 86% rename from src/code/util/StringUtils.java rename to src/main/java/com/alex_zaitsev/adg/util/StringUtils.java index c43ed1b..e82be48 100644 --- a/src/code/util/StringUtils.java +++ b/src/main/java/com/alex_zaitsev/adg/util/StringUtils.java @@ -1,4 +1,4 @@ -package code.util; +package com.alex_zaitsev.adg.util; public class StringUtils { public static boolean isNumber(String str) { diff --git a/src/code/util/ZipFileUtils.java b/src/main/java/com/alex_zaitsev/adg/util/ZipFileUtils.java similarity index 96% rename from src/code/util/ZipFileUtils.java rename to src/main/java/com/alex_zaitsev/adg/util/ZipFileUtils.java index 99b4ec3..85dc13c 100644 --- a/src/code/util/ZipFileUtils.java +++ b/src/main/java/com/alex_zaitsev/adg/util/ZipFileUtils.java @@ -1,4 +1,4 @@ -package code.util; +package com.alex_zaitsev.adg.util; import java.io.IOException; import java.util.ArrayList; diff --git a/src/test/java/ArgumentReaderTests.java b/src/test/java/ArgumentReaderTests.java new file mode 100644 index 0000000..ad10748 --- /dev/null +++ b/src/test/java/ArgumentReaderTests.java @@ -0,0 +1,137 @@ +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.io.File; +import java.io.IOException; + +import org.junit.*; +import org.junit.rules.TemporaryFolder; +import org.hamcrest.core.StringContains; + +import com.alex_zaitsev.adg.io.ArgumentReader; +import com.alex_zaitsev.adg.io.Arguments; + +public class ArgumentReaderTests { + + @Rule + public TemporaryFolder folder= new TemporaryFolder(); + + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + private final PrintStream originalErr = System.err; + private File apkFile; + private File filterFile; + + @Before + public void setUp() throws IOException { + System.setOut(new PrintStream(outContent)); + System.setErr(new PrintStream(errContent)); + + apkFile = folder.newFile("test.apk"); + filterFile = folder.newFile("filter.json"); + } + + @After + public void teardown() { + System.setOut(originalOut); + System.setErr(originalErr); + } + + /** + * When wrong or incomplete arguments are passed + * to ArgumentReader, read() must return Null and print error message. + */ + @Test + public void wrongArgumentsReturnsNullAndPrintsMessage() { + String[] inputArgs = new String[] {"-i", "/path/"}; + ArgumentReader sut = new ArgumentReader(inputArgs); + + Arguments args = sut.read(); + + assertThat(args, nullValue()); + assertThat(errContent.toString(), containsString("must be provided!")); + assertThat(errContent.toString(), containsString("Usage:")); + } + + /** + * When non-existing Apk file path is passed + * to ArgumentReader, read() must return Null and print error message. + */ + @Test + public void nonExistingApkPathReturnsNullAndPrintsMessage() { + String[] inputArgs = new String[] {"-i", "/path/", "-o", "/path/", "-a", "/wrong/"}; + ArgumentReader sut = new ArgumentReader(inputArgs); + + Arguments args = sut.read(); + + assertThat(args, nullValue()); + String message = File.separator + "wrong is not found!"; + assertThat(errContent.toString(), containsString(message)); + } + + /** + * When wrong filter is passed to ArgumentReader, + * read() must return Arguments and print error message. + */ + @Test + public void wrongFilterFileReturnsNullAndPrintsMessage() { + String[] inputArgs = new String[] {"-i", "/path/", "-o", "/path/", + "-a", apkFile.getAbsolutePath(), + "-f", "/wrong/"}; + ArgumentReader sut = new ArgumentReader(inputArgs); + + Arguments args = sut.read(); + + assertThat(args, nullValue()); + String message = File.separator + "wrong is not found!"; + assertThat(errContent.toString(), containsString(message)); + } + + /** + * When correct arguments without filter are passed + * to ArgumentReader, read() must return correct Arguments. + */ + @Test + public void correctArgumentsWithoutFiltersReturnsArguments() { + String projectPath = "/pathI/"; + String resultPath = "/pathO/"; + String apkPath = apkFile.getAbsolutePath(); + String[] inputArgs = new String[] {"-i", projectPath, "-o", resultPath, + "-a", apkPath}; + ArgumentReader sut = new ArgumentReader(inputArgs); + + Arguments args = sut.read(); + + assertThat(args, notNullValue()); + assertThat(args.getFiltersPath(), nullValue()); + assertThat(args.getProjectPath(), equalTo(projectPath)); + assertThat(args.getResultPath(), equalTo(resultPath)); + assertThat(args.getApkFilePath(), equalTo(apkPath)); + } + + /** + * When correct arguments are passed to ArgumentReader, + * read() must return correct Arguments. + */ + @Test + public void correctArgumentsReturnsArguments() { + String projectPath = "/pathI/"; + String resultPath = "/pathO/"; + String apkPath = apkFile.getAbsolutePath(); + String filtersPath = filterFile.getAbsolutePath(); + String[] inputArgs = new String[] {"-i", projectPath, "-o", resultPath, + "-a", apkPath, "-f", filtersPath}; + ArgumentReader sut = new ArgumentReader(inputArgs); + + Arguments args = sut.read(); + + assertThat(args, notNullValue()); + assertThat(args.getProjectPath(), equalTo(projectPath)); + assertThat(args.getResultPath(), equalTo(resultPath)); + assertThat(args.getApkFilePath(), equalTo(apkPath)); + assertThat(args.getFiltersPath(), equalTo(filtersPath)); + } +} \ No newline at end of file diff --git a/src/test/java/FilterProviderTests.java b/src/test/java/FilterProviderTests.java new file mode 100644 index 0000000..5d0e9aa --- /dev/null +++ b/src/test/java/FilterProviderTests.java @@ -0,0 +1,127 @@ +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +import java.io.*; + +import org.junit.*; +import org.hamcrest.core.*; + +import com.alex_zaitsev.adg.filter.*; +import com.alex_zaitsev.adg.io.*; +import com.alex_zaitsev.adg.*; + +public class FilterProviderTests { + + private Filters defaultFilters; + + private String getPath(String original) { + return original.replace('/', File.separatorChar); + } + + @Before + public void setUp() throws IOException { + String packageName = "com.example.package"; + String[] ignoredClasses = new String[] {".*Dagger.*", ".*Injector.*", ".*\\$_ViewBinding$", ".*_Factory$"}; + defaultFilters = new Filters(packageName, Filters.DEFAULT_PROCESS_INNER, ignoredClasses); + } + + /** + * If package name is null, `makePathFilter` returns null. + */ + @Test + public void makePathFilterReturnsNullIfPackageNameIsNull() { + defaultFilters.setPackageName(null); + FilterProvider sut = new FilterProvider(defaultFilters); + Filter filter = sut.makePathFilter(); + + assertThat(filter, nullValue()); + } + + /** + * If package name is empty, `makePathFilter` returns null. + */ + @Test + public void makePathFilterReturnsNullIfPackageNameIsEmpty() { + defaultFilters.setPackageName(""); + FilterProvider sut = new FilterProvider(defaultFilters); + Filter filter = sut.makePathFilter(); + + assertThat(filter, nullValue()); + } + + /** + * If provided filters are ok, `makePathFilter` returns expected Filter. + */ + @Test + public void makePathFilterReturnsExpectedFilter() { + FilterProvider sut = new FilterProvider(defaultFilters); + Filter filter = sut.makePathFilter(); + + assertThat(filter, notNullValue()); + String filterStringRepr = File.separatorChar == '/' ? + "RegexFilter{.*com/example/package.*}" : // for Unix + "RegexFilter{.*com\\\\example\\\\package.*}"; // for Windows + assertThat(filter.toString(), equalTo(filterStringRepr)); + } + + /** + * If provided filters are ok, `makePathFilter` returns Filter + * that filters as expected. + */ + @Test + public void makePathFilterReturnsFilterThatFiltersAsExpected() { + FilterProvider sut = new FilterProvider(defaultFilters); + Filter filter = sut.makePathFilter(); + + assertThat(filter, notNullValue()); + + String correctPath1 = getPath("com/example/package"); + assertThat(filter.filter(correctPath1), is(true)); + String correctPath2 = getPath("some/path/com/example/package/inner"); + assertThat(filter.filter(correctPath2), is(true)); + + String wrongPath1 = getPath("com/example/wrong"); + assertThat(filter.filter(wrongPath1), is(false)); + String wrongPath2 = getPath("com/wrong/package"); + assertThat(filter.filter(wrongPath2), is(false)); + } + + /** + * If provided filters are ok, `makeClassFilter` returns AndFilter with + * InverseRegexFilter and RegexFilter. + */ + @Test + public void makeClassFilterReturnsExpectedFilter() { + FilterProvider sut = new FilterProvider(defaultFilters); + Filter filter = sut.makeClassFilter(); + + assertThat(filter, notNullValue()); + String inverseRegexFilter = "InverseRegexFilter{.*Dagger.*|.*Injector.*|.*\\$_ViewBinding$|.*_Factory$}"; + assertThat(filter.toString(), equalTo(inverseRegexFilter)); + } + + /** + * If provided filters are ok, `makeClassFilter` returns Filter + * that filters as expected. + */ + @Test + public void makeClassFilterReturnsFilterThatFiltersAsExpected() { + FilterProvider sut = new FilterProvider(defaultFilters); + Filter filter = sut.makeClassFilter(); + + assertThat(filter, notNullValue()); + + assertThat(filter, notNullValue()); + String notPassing1 = "ClassDagger"; + assertThat(filter.filter(notPassing1), is(false)); + String notPassing2 = "SomeInjectorClass"; + assertThat(filter.filter(notPassing2), is(false)); + String notPassing3 = "QrCodeZxingMvpPresenterImpl_Factory"; + assertThat(filter.filter(notPassing3), is(false)); + String notPassing4 = "Some$_ViewBinding"; + assertThat(filter.filter(notPassing4), is(false)); + + String passing = "SomeClass"; + assertThat(filter.filter(passing), is(true)); + } +} \ No newline at end of file diff --git a/src/test/java/FiltersReaderTests.java b/src/test/java/FiltersReaderTests.java new file mode 100644 index 0000000..1d0b9a9 --- /dev/null +++ b/src/test/java/FiltersReaderTests.java @@ -0,0 +1,187 @@ +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.io.File; +import java.io.IOException; +import java.io.FileWriter; + +import org.junit.*; +import org.junit.rules.TemporaryFolder; +import org.hamcrest.core.StringContains; + +import com.alex_zaitsev.adg.io.FiltersReader; +import com.alex_zaitsev.adg.io.Filters; + +public class FiltersReaderTests { + + private static final String PACKAGE_NAME = "com.example.package"; + private static final boolean SHOW_INNER_CLASSES_DEFAULT = false; + private static final boolean SHOW_INNER_CLASSES_TRUE = true; + private static final String[] IGNORED_CLASSES_DEFAULT = new String[] { + ".*Dagger.*", ".*Injector.*", ".*\\$_ViewBinding$", ".*_Factory$" + }; + private static final String FILTERS_DEFAULT = "{" + + "\"package-name\": \"\"," + + "\"show-inner-classes\": false," + + "\"ignored-classes\": [\".*Dagger.*\", \".*Injector.*\", \".*\\$_ViewBinding$\", \".*_Factory$\"]" + + "}"; + private static final String FILTERS_WITHOUT_PACKAGE_NAME = "{" + + "\"show-inner-classes\": false," + + "\"ignored-classes\": [\".*Dagger.*\", \".*Injector.*\", \".*\\$_ViewBinding$\", \".*_Factory$\"]" + + "}"; + private static final String FILTERS_SHOW_INNER_TRUE = "{" + + "\"package-name\": \"com.example.package\"," + + "\"show-inner-classes\": true," + + "\"ignored-classes\": [\".*Dagger.*\", \".*Injector.*\", \".*\\$_ViewBinding$\", \".*_Factory$\"]" + + "}"; + private static final String FILTERS_FULL = "{" + + "\"package-name\": \"com.example.package\"," + + "\"show-inner-classes\": false," + + "\"ignored-classes\": [\".*Dagger.*\", \".*Injector.*\", \".*\\$_ViewBinding$\", \".*_Factory$\"]" + + "}"; + private static final String FILTERS_MALFORMED = "{\"malformed\"}"; + + @Rule + public TemporaryFolder folder= new TemporaryFolder(); + + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + private final PrintStream originalErr = System.err; + + private File filterFile; + + @Before + public void setUp() throws IOException { + System.setOut(new PrintStream(outContent)); + System.setErr(new PrintStream(errContent)); + + filterFile = folder.newFile("default.json"); + } + + @After + public void teardown() { + System.setOut(originalOut); + System.setErr(originalErr); + } + + /** + * When wrong file is passed to FiltersReader, + * read() must return null and print error message. + */ + @Test + public void wrongFileReturnsNullAndPrintsMessage() { + String wrongFilePath = "/wrong/"; + FiltersReader sut = new FiltersReader(wrongFilePath); + + Filters filters = sut.read(); + + assertThat(filters, nullValue()); + String message = "An error happened during " + wrongFilePath + " processing!"; + assertThat(errContent.toString(), containsString(message)); + } + + /** + * When malformed filter file is passed to FiltersReader, + * read() must return null and print error message. + */ + @Test + public void malformedFiltersFileReturnsNullAndPrintsMessage() throws IOException { + FileWriter fw = new FileWriter(filterFile); + fw.write(FILTERS_MALFORMED); + fw.close(); + FiltersReader sut = new FiltersReader(filterFile.getAbsolutePath()); + + Filters filters = sut.read(); + + assertThat(filters, nullValue()); + String message = "An error happened during " + filterFile.getAbsolutePath() + " processing!"; + assertThat(errContent.toString(), containsString(message)); + } + + /** + * When correct file with empty 'package-name' parameter + * is passed to FiltersReader, read() must return correct Filters + * and print info message. + */ + @Test + public void correctFiltersFileWithEmptyPackageNameReturnsFiltersAndPrintsMessage() throws IOException { + FileWriter fw = new FileWriter(filterFile); + fw.write(FILTERS_DEFAULT); + fw.close(); + FiltersReader sut = new FiltersReader(filterFile.getAbsolutePath()); + + Filters filters = sut.read(); + + assertThat(filters, notNullValue()); + assertThat(filters.getPackageName(), nullValue()); + assertThat(filters.isProcessingInner(), equalTo(SHOW_INNER_CLASSES_DEFAULT)); + assertThat(filters.getIgnoredClasses(), equalTo(IGNORED_CLASSES_DEFAULT)); + String message = "Warning! Processing without package filter."; + assertThat(outContent.toString(), containsString(message)); + } + + /** + * When filter file without 'package-name' parameter + * is passed to FiltersReader, read() must return correct Filters + * and print info message. + */ + @Test + public void correctFiltersFileWithoutPackageNameReturnsFiltersAndPrintsMessage() throws IOException { + FileWriter fw = new FileWriter(filterFile); + fw.write(FILTERS_WITHOUT_PACKAGE_NAME); + fw.close(); + FiltersReader sut = new FiltersReader(filterFile.getAbsolutePath()); + + Filters filters = sut.read(); + + assertThat(filters, notNullValue()); + assertThat(filters.getPackageName(), nullValue()); + assertThat(filters.isProcessingInner(), equalTo(SHOW_INNER_CLASSES_DEFAULT)); + assertThat(filters.getIgnoredClasses(), equalTo(IGNORED_CLASSES_DEFAULT)); + String message = "Warning! Processing without package filter."; + assertThat(outContent.toString(), containsString(message)); + } + + /** + * When filter file with full parameters + * is passed to FiltersReader, read() must return correct Filters. + */ + @Test + public void correctFiltersFileReturnsFilters() throws IOException { + FileWriter fw = new FileWriter(filterFile); + fw.write(FILTERS_FULL); + fw.close(); + FiltersReader sut = new FiltersReader(filterFile.getAbsolutePath()); + + Filters filters = sut.read(); + + assertThat(filters, notNullValue()); + assertThat(filters.getPackageName(), equalTo(PACKAGE_NAME)); + assertThat(filters.isProcessingInner(), equalTo(SHOW_INNER_CLASSES_DEFAULT)); + assertThat(filters.getIgnoredClasses(), equalTo(IGNORED_CLASSES_DEFAULT)); + } + + /** + * When `show-inner-classes` option is enabled, + * read() must return correct Filters and info message is printed. + */ + @Test + public void correctFiltersFileWithEnabledInnerClassesPrintsMessage() throws IOException { + FileWriter fw = new FileWriter(filterFile); + fw.write(FILTERS_SHOW_INNER_TRUE); + fw.close(); + FiltersReader sut = new FiltersReader(filterFile.getAbsolutePath()); + + Filters filters = sut.read(); + + assertThat(filters, notNullValue()); + assertThat(filters.getPackageName(), equalTo(PACKAGE_NAME)); + assertThat(filters.isProcessingInner(), equalTo(SHOW_INNER_CLASSES_TRUE)); + assertThat(filters.getIgnoredClasses(), equalTo(IGNORED_CLASSES_DEFAULT)); + String message = "Warning! Processing including inner classes."; + assertThat(outContent.toString(), containsString(message)); + } +} \ No newline at end of file diff --git a/src/test/java/SmaliAnalyzerTests.java b/src/test/java/SmaliAnalyzerTests.java new file mode 100644 index 0000000..6850ef5 --- /dev/null +++ b/src/test/java/SmaliAnalyzerTests.java @@ -0,0 +1,120 @@ +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.io.File; +import java.io.IOException; +import java.io.FileWriter; + +import org.junit.*; +import org.junit.rules.TemporaryFolder; +import org.hamcrest.core.StringContains; +import org.hamcrest.core.Is; + +import com.alex_zaitsev.adg.io.Arguments; +import com.alex_zaitsev.adg.io.Filters; +import com.alex_zaitsev.adg.SmaliAnalyzer; + +public class SmaliAnalyzerTests { + + @Rule + public TemporaryFolder folder= new TemporaryFolder(); + + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + private final PrintStream originalErr = System.err; + + private Arguments defaultArguments; + private Filters defaultFilters; + + @Before + public void setUp() throws IOException { + System.setOut(new PrintStream(outContent)); + System.setErr(new PrintStream(errContent)); + + String apkPath = folder.newFile("application.apk").getAbsolutePath(); + String projectPath = folder.newFolder("project").getAbsolutePath(); + String outputPath = folder.newFolder("result").getAbsolutePath(); + String filtersPath = folder.newFile("filters.json").getAbsolutePath(); + defaultArguments = new Arguments(apkPath, projectPath, outputPath, filtersPath); + + String packageName = "com.example.package"; + String[] ignoredClasses = new String[] { + ".*Dagger.*", ".*Injector.*", ".*\\$_ViewBinding$", ".*_Factory$" + }; + defaultFilters = new Filters(packageName, Filters.DEFAULT_PROCESS_INNER, + ignoredClasses); + } + + @After + public void teardown() { + System.setOut(originalOut); + System.setErr(originalErr); + } + + /** + * When `run` method is called, intro info message is printed. + */ + @Test + public void runPrintsIntroMessage() { + SmaliAnalyzer sut = new SmaliAnalyzer(defaultArguments, defaultFilters, null, null); + + sut.run(); + + String message = "Analyzing dependencies..."; + assertThat(outContent.toString(), containsString(message)); + } + + /** + * If project folder does not exist, error message is printed + * and `run` returns false. + */ + @Test + public void runPrintsMessageAndReturnsFalseIfProjectFolderIsAbsent() throws IOException { + Arguments argsWihNonExistingPath = defaultArguments; + File nonExisting = new File("non-existing"); + argsWihNonExistingPath.setProjectPath(nonExisting.getAbsolutePath()); + SmaliAnalyzer sut = new SmaliAnalyzer(argsWihNonExistingPath, defaultFilters, null, null); + + boolean result = sut.run(); + + String message = nonExisting + " does not exist!"; + assertThat(errContent.toString(), containsString(message)); + assertThat(result, is(false)); + } + + /** + * If apk uses instant run, error message is printed + * and `run` returns false. + */ + @Test + public void runPrintsMessageAndReturnsFalseIfApkUsesInstantRun() throws IOException { + File project = new File(defaultArguments.getProjectPath()); + File unknown = new File(project, "unknown"); + unknown.mkdir(); + File unknownZip = new File(unknown, "instant-run.zip"); + unknownZip.createNewFile(); + SmaliAnalyzer sut = new SmaliAnalyzer(defaultArguments, defaultFilters, null, null); + + boolean result = sut.run(); + + String message = "Enabled Instant Run feature detected."; + assertThat(errContent.toString(), containsString(message)); + assertThat(result, is(false)); + } + + /** + * If good params were provided to `SmaliAnalyzer`, + * `run` returns true. + */ + @Test + public void runWithDefaultParamsReturnsTrue() { + SmaliAnalyzer sut = new SmaliAnalyzer(defaultArguments, defaultFilters, null, null); + + boolean result = sut.run(); + + assertThat(result, is(true)); + } +} \ No newline at end of file