├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── LICENSE.txt ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main ├── java └── com │ └── android │ └── dexdeps │ ├── ClassRef.java │ ├── DexData.java │ ├── DexDataException.java │ ├── FieldRef.java │ ├── HasDeclaringClass.java │ ├── MethodRef.java │ └── Output.java ├── kotlin └── au │ └── com │ └── timmutton │ └── redexplugin │ ├── DexFile.kt │ ├── IOUtil.kt │ ├── RedexDownloadTask.kt │ ├── RedexExtension.kt │ ├── RedexPlugin.kt │ ├── RedexTask.kt │ └── internal │ ├── RedexConfiguration.kt │ └── RedexConfigurationContainer.kt └── resources └── META-INF └── gradle-plugins └── redex.properties /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | 10 | # vim temporary files 11 | *.swp 12 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | redex -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 26 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | Android Lint 46 | 47 | 48 | C/C++ 49 | 50 | 51 | Control flow issuesJava 52 | 53 | 54 | GeneralC/C++ 55 | 56 | 57 | Java 58 | 59 | 60 | Probable bugsJava 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 82 | 83 | 84 | 85 | 86 | 1.8 87 | 88 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tim Mutton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redex Plugin 2 | 3 | A Gradle plugin that allows you to use Facebook's Redex tool as part of your 4 | build process 5 | 6 | ## Usage 7 | Add the following to your `build.gradle`: 8 | 9 | ```groovy 10 | buildscript { 11 | repositories { 12 | jcenter() 13 | } 14 | dependencies { 15 | classpath 'com.android.tools.build:gradle:2.0.0' 16 | classpath 'au.com.timmutton:redex:1.5.0' 17 | } 18 | } 19 | 20 | apply plugin: 'com.android.application' 21 | apply plugin: 'redex' 22 | 23 | // Optional: configure redex arguments 24 | // These are example arguments, fill them with your specific arguments 25 | redex { 26 | // see github.com/facebook/redex/blob/stable/config/default.config 27 | configFile = new File('redex.config') 28 | 29 | // 'latest' downloads the most recent release of redex (recommended) 30 | // `null` does not download redex at all. Assumes `redex` is in PATH 31 | // Any other version string will download that specific version of redex 32 | // from github.com/facebook/redex/releases/tag/ 33 | version = 'latest' 34 | 35 | // `passes` is shorthand for a pass list instead of a config file 36 | // passes = ['ReBindRefsPass', ..., 'ShortenSrcStringsPass'] 37 | 38 | proguardConfigFiles = [new File('common_proguard.pro'), 39 | new File('my_app_proguard.pro')] 40 | proguardMapFile = new File('proguard_map.txt') 41 | keepFile = new File('keep.txt') 42 | 43 | jarFiles = [new File('lib1.jar'), new File('lib2.jar')] 44 | otherArgs = '' // any other command line options to `redex` 45 | 46 | // see `redex --help` for details on these arguments 47 | } 48 | 49 | ``` 50 | If you do not set the passes or the config file, the default set of passes will 51 | be run. Sometimes you may not want to run all optimisation passes, for example 52 | some appear to break when optimising kotlin code. 53 | 54 | If you specified a signing configuration for the given build type, this plugin 55 | will use that configuration to re-sign the application (Redex normally un-signs 56 | the apk). 57 | 58 | ## License 59 | The MIT License (MIT) 60 | 61 | Copyright (c) 2016 Tim Mutton 62 | 63 | Permission is hereby granted, free of charge, to any person obtaining a copy 64 | of this software and associated documentation files (the "Software"), to deal 65 | in the Software without restriction, including without limitation the rights 66 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 67 | copies of the Software, and to permit persons to whom the Software is 68 | furnished to do so, subject to the following conditions: 69 | 70 | The above copyright notice and this permission notice shall be included in all 71 | copies or substantial portions of the Software. 72 | 73 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 74 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 75 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 76 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 77 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 78 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 79 | SOFTWARE. 80 | 81 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | kotlinVersion = '1.1.3' 4 | } 5 | 6 | repositories { 7 | google() 8 | jcenter() 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" 14 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3' 15 | } 16 | } 17 | 18 | apply plugin: 'idea' 19 | apply plugin: 'kotlin' 20 | apply plugin: 'maven' 21 | apply plugin: 'com.jfrog.bintray' 22 | 23 | group = "au.com.timmutton" 24 | version = "1.4.0" 25 | 26 | dependencies { 27 | repositories { 28 | google() 29 | mavenCentral() 30 | jcenter() 31 | } 32 | 33 | compile gradleApi() 34 | compile 'com.android.tools.build:gradle:2.3.3' 35 | compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" 36 | compile 'com.google.code.gson:gson:2.8.0' 37 | compile 'com.github.kittinunf.fuel:fuel:1.11.0' 38 | compile 'com.github.kittinunf.fuel:fuel-gson:1.11.0' 39 | compile 'com.github.kittinunf.result:result:1.2.0' 40 | } 41 | 42 | task wrapper(type: Wrapper) { 43 | gradleVersion = '4.2' 44 | distributionUrl = "https://services.gradle.org/distributions/gradle-${gradleVersion}-all.zip" 45 | } 46 | 47 | task sourcesJar(type: Jar) { 48 | description = 'An archive of the source code for Maven Central' 49 | from sourceSets.main.kotlin 50 | classifier = 'sources' 51 | } 52 | 53 | task javadocJar(type: Jar, dependsOn: javadoc) { 54 | classifier = 'javadoc' 55 | from javadoc 56 | } 57 | 58 | artifacts { 59 | archives javadocJar, sourcesJar 60 | } 61 | 62 | // Bintray 63 | Properties properties = new Properties() 64 | File properties_file = project.rootProject.file('local.properties') 65 | if (properties_file.exists()) { 66 | properties.load(properties_file.newDataInputStream()) 67 | } 68 | 69 | bintray { 70 | user = properties.getProperty("bintray.user") 71 | key = properties.getProperty("bintray.apikey") 72 | 73 | configurations = ['archives'] 74 | 75 | pkg { 76 | repo = 'maven' 77 | name = 'redex' 78 | desc = 'A gradle plugin that allows you to use the Redex tool as part of your build process' 79 | websiteUrl = 'https://github.com/timmutton/redex-plugin/' 80 | vcsUrl = 'https://github.com/outware/redex-plugin.git' 81 | licenses = ["MIT"] 82 | publicDownloadNumbers = true 83 | publish = true 84 | } 85 | } 86 | 87 | bintrayUpload.dependsOn(install) 88 | install.dependsOn(build) 89 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmutton/redex-plugin/87a0d9aa5972556fba326f079c8de1229d667ef8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.2.1-bin.zip 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'redex' -------------------------------------------------------------------------------- /src/main/java/com/android/dexdeps/ClassRef.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2010 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.dexdeps; 18 | 19 | import java.util.ArrayList; 20 | 21 | public class ClassRef { 22 | private String mClassName; 23 | private ArrayList mFieldRefs; 24 | private ArrayList mMethodRefs; 25 | 26 | /** 27 | * Initializes a new class reference. 28 | */ 29 | public ClassRef(String className) { 30 | mClassName = className; 31 | mFieldRefs = new ArrayList(); 32 | mMethodRefs = new ArrayList(); 33 | } 34 | 35 | /** 36 | * Adds the field to the field list. 37 | */ 38 | public void addField(FieldRef fref) { 39 | mFieldRefs.add(fref); 40 | } 41 | 42 | /** 43 | * Returns the field list as an array. 44 | */ 45 | public FieldRef[] getFieldArray() { 46 | return mFieldRefs.toArray(new FieldRef[mFieldRefs.size()]); 47 | } 48 | 49 | /** 50 | * Adds the method to the method list. 51 | */ 52 | public void addMethod(MethodRef mref) { 53 | mMethodRefs.add(mref); 54 | } 55 | 56 | /** 57 | * Returns the method list as an array. 58 | */ 59 | public MethodRef[] getMethodArray() { 60 | return mMethodRefs.toArray(new MethodRef[mMethodRefs.size()]); 61 | } 62 | 63 | /** 64 | * Gets the class name. 65 | */ 66 | public String getName() { 67 | return mClassName; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/android/dexdeps/DexData.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009 The Android Open Source Project 3 | * Copyright (C) 2015-2017 Keepsafe Software 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package com.android.dexdeps; 19 | 20 | import java.io.IOException; 21 | import java.io.RandomAccessFile; 22 | import java.nio.charset.StandardCharsets; 23 | import java.util.Arrays; 24 | 25 | /** 26 | * Data extracted from a DEX file. 27 | */ 28 | public class DexData { 29 | private RandomAccessFile mDexFile; 30 | private HeaderItem mHeaderItem; 31 | private String[] mStrings; // strings from string_data_* 32 | private TypeIdItem[] mTypeIds; 33 | private ProtoIdItem[] mProtoIds; 34 | private FieldIdItem[] mFieldIds; 35 | private MethodIdItem[] mMethodIds; 36 | private ClassDefItem[] mClassDefs; 37 | 38 | private byte tmpBuf[] = new byte[4]; 39 | private boolean isBigEndian = false; 40 | 41 | /** 42 | * Constructs a new DexData for this file. 43 | */ 44 | public DexData(RandomAccessFile raf) { 45 | mDexFile = raf; 46 | } 47 | 48 | /** 49 | * Loads the contents of the DEX file into our data structures. 50 | * 51 | * @throws IOException if we encounter a problem while reading 52 | * @throws DexDataException if the DEX contents look bad 53 | */ 54 | public void load() throws IOException { 55 | parseHeaderItem(); 56 | 57 | loadStrings(); 58 | loadTypeIds(); 59 | loadProtoIds(); 60 | loadFieldIds(); 61 | loadMethodIds(); 62 | loadClassDefs(); 63 | 64 | markInternalClasses(); 65 | } 66 | 67 | /** 68 | * Verifies the given magic number. 69 | */ 70 | private static boolean verifyMagic(byte[] magic) { 71 | return Arrays.equals(magic, HeaderItem.DEX_FILE_MAGIC_v035) || 72 | Arrays.equals(magic, HeaderItem.DEX_FILE_MAGIC_v037); 73 | } 74 | 75 | /** 76 | * Parses the interesting bits out of the header. 77 | */ 78 | void parseHeaderItem() throws IOException { 79 | mHeaderItem = new HeaderItem(); 80 | 81 | seek(0); 82 | 83 | byte[] magic = new byte[8]; 84 | readBytes(magic); 85 | if (!verifyMagic(magic)) { 86 | System.err.println("Magic number is wrong -- are you sure " + 87 | "this is a DEX file?"); 88 | throw new DexDataException(); 89 | } 90 | 91 | /* 92 | * Read the endian tag, so we properly swap things as we read 93 | * them from here on. 94 | */ 95 | seek(8+4+20+4+4); 96 | mHeaderItem.endianTag = readInt(); 97 | if (mHeaderItem.endianTag == HeaderItem.ENDIAN_CONSTANT) { 98 | /* do nothing */ 99 | } else if (mHeaderItem.endianTag == HeaderItem.REVERSE_ENDIAN_CONSTANT){ 100 | /* file is big-endian (!), reverse future reads */ 101 | isBigEndian = true; 102 | } else { 103 | System.err.println("Endian constant has unexpected value " + 104 | Integer.toHexString(mHeaderItem.endianTag)); 105 | throw new DexDataException(); 106 | } 107 | 108 | seek(8+4+20); // magic, checksum, signature 109 | mHeaderItem.fileSize = readInt(); 110 | mHeaderItem.headerSize = readInt(); 111 | /*mHeaderItem.endianTag =*/ readInt(); 112 | /*mHeaderItem.linkSize =*/ readInt(); 113 | /*mHeaderItem.linkOff =*/ readInt(); 114 | /*mHeaderItem.mapOff =*/ readInt(); 115 | mHeaderItem.stringIdsSize = readInt(); 116 | mHeaderItem.stringIdsOff = readInt(); 117 | mHeaderItem.typeIdsSize = readInt(); 118 | mHeaderItem.typeIdsOff = readInt(); 119 | mHeaderItem.protoIdsSize = readInt(); 120 | mHeaderItem.protoIdsOff = readInt(); 121 | mHeaderItem.fieldIdsSize = readInt(); 122 | mHeaderItem.fieldIdsOff = readInt(); 123 | mHeaderItem.methodIdsSize = readInt(); 124 | mHeaderItem.methodIdsOff = readInt(); 125 | mHeaderItem.classDefsSize = readInt(); 126 | mHeaderItem.classDefsOff = readInt(); 127 | /*mHeaderItem.dataSize =*/ readInt(); 128 | /*mHeaderItem.dataOff =*/ readInt(); 129 | } 130 | 131 | /** 132 | * Loads the string table out of the DEX. 133 | * 134 | * First we read all of the string_id_items, then we read all of the 135 | * string_data_item. Doing it this way should allow us to avoid 136 | * seeking around in the file. 137 | */ 138 | void loadStrings() throws IOException { 139 | int count = mHeaderItem.stringIdsSize; 140 | int stringOffsets[] = new int[count]; 141 | 142 | //System.out.println("reading " + count + " strings"); 143 | 144 | seek(mHeaderItem.stringIdsOff); 145 | for (int i = 0; i < count; i++) { 146 | stringOffsets[i] = readInt(); 147 | } 148 | 149 | mStrings = new String[count]; 150 | 151 | seek(stringOffsets[0]); 152 | for (int i = 0; i < count; i++) { 153 | seek(stringOffsets[i]); // should be a no-op 154 | mStrings[i] = readString(); 155 | //System.out.println("STR: " + i + ": " + mStrings[i]); 156 | } 157 | } 158 | 159 | /** 160 | * Loads the type ID list. 161 | */ 162 | void loadTypeIds() throws IOException { 163 | int count = mHeaderItem.typeIdsSize; 164 | mTypeIds = new TypeIdItem[count]; 165 | 166 | //System.out.println("reading " + count + " typeIds"); 167 | seek(mHeaderItem.typeIdsOff); 168 | for (int i = 0; i < count; i++) { 169 | mTypeIds[i] = new TypeIdItem(); 170 | mTypeIds[i].descriptorIdx = readInt(); 171 | 172 | //System.out.println(i + ": " + mTypeIds[i].descriptorIdx + 173 | // " " + mStrings[mTypeIds[i].descriptorIdx]); 174 | } 175 | } 176 | 177 | /** 178 | * Loads the proto ID list. 179 | */ 180 | void loadProtoIds() throws IOException { 181 | int count = mHeaderItem.protoIdsSize; 182 | mProtoIds = new ProtoIdItem[count]; 183 | 184 | //System.out.println("reading " + count + " protoIds"); 185 | seek(mHeaderItem.protoIdsOff); 186 | 187 | /* 188 | * Read the proto ID items. 189 | */ 190 | for (int i = 0; i < count; i++) { 191 | mProtoIds[i] = new ProtoIdItem(); 192 | mProtoIds[i].shortyIdx = readInt(); 193 | mProtoIds[i].returnTypeIdx = readInt(); 194 | mProtoIds[i].parametersOff = readInt(); 195 | 196 | //System.out.println(i + ": " + mProtoIds[i].shortyIdx + 197 | // " " + mStrings[mProtoIds[i].shortyIdx]); 198 | } 199 | 200 | /* 201 | * Go back through and read the type lists. 202 | */ 203 | for (int i = 0; i < count; i++) { 204 | ProtoIdItem protoId = mProtoIds[i]; 205 | 206 | int offset = protoId.parametersOff; 207 | 208 | if (offset == 0) { 209 | protoId.types = new int[0]; 210 | continue; 211 | } else { 212 | seek(offset); 213 | int size = readInt(); // #of entries in list 214 | protoId.types = new int[size]; 215 | 216 | for (int j = 0; j < size; j++) { 217 | protoId.types[j] = readShort() & 0xffff; 218 | } 219 | } 220 | } 221 | } 222 | 223 | /** 224 | * Loads the field ID list. 225 | */ 226 | void loadFieldIds() throws IOException { 227 | int count = mHeaderItem.fieldIdsSize; 228 | mFieldIds = new FieldIdItem[count]; 229 | 230 | //System.out.println("reading " + count + " fieldIds"); 231 | seek(mHeaderItem.fieldIdsOff); 232 | for (int i = 0; i < count; i++) { 233 | mFieldIds[i] = new FieldIdItem(); 234 | mFieldIds[i].classIdx = readShort() & 0xffff; 235 | mFieldIds[i].typeIdx = readShort() & 0xffff; 236 | mFieldIds[i].nameIdx = readInt(); 237 | 238 | //System.out.println(i + ": " + mFieldIds[i].nameIdx + 239 | // " " + mStrings[mFieldIds[i].nameIdx]); 240 | } 241 | } 242 | 243 | /** 244 | * Loads the method ID list. 245 | */ 246 | void loadMethodIds() throws IOException { 247 | int count = mHeaderItem.methodIdsSize; 248 | mMethodIds = new MethodIdItem[count]; 249 | 250 | //System.out.println("reading " + count + " methodIds"); 251 | seek(mHeaderItem.methodIdsOff); 252 | for (int i = 0; i < count; i++) { 253 | mMethodIds[i] = new MethodIdItem(); 254 | mMethodIds[i].classIdx = readShort() & 0xffff; 255 | mMethodIds[i].protoIdx = readShort() & 0xffff; 256 | mMethodIds[i].nameIdx = readInt(); 257 | 258 | //System.out.println(i + ": " + mMethodIds[i].nameIdx + 259 | // " " + mStrings[mMethodIds[i].nameIdx]); 260 | } 261 | } 262 | 263 | /** 264 | * Loads the class defs list. 265 | */ 266 | void loadClassDefs() throws IOException { 267 | int count = mHeaderItem.classDefsSize; 268 | mClassDefs = new ClassDefItem[count]; 269 | 270 | //System.out.println("reading " + count + " classDefs"); 271 | seek(mHeaderItem.classDefsOff); 272 | for (int i = 0; i < count; i++) { 273 | mClassDefs[i] = new ClassDefItem(); 274 | mClassDefs[i].classIdx = readInt(); 275 | 276 | /* access_flags = */ readInt(); 277 | /* superclass_idx = */ readInt(); 278 | /* interfaces_off = */ readInt(); 279 | /* source_file_idx = */ readInt(); 280 | /* annotations_off = */ readInt(); 281 | /* class_data_off = */ readInt(); 282 | /* static_values_off = */ readInt(); 283 | 284 | //System.out.println(i + ": " + mClassDefs[i].classIdx + " " + 285 | // mStrings[mTypeIds[mClassDefs[i].classIdx].descriptorIdx]); 286 | } 287 | } 288 | 289 | /** 290 | * Sets the "internal" flag on type IDs which are defined in the 291 | * DEX file or within the VM (e.g. primitive classes and arrays). 292 | */ 293 | void markInternalClasses() { 294 | for (int i = mClassDefs.length -1; i >= 0; i--) { 295 | mTypeIds[mClassDefs[i].classIdx].internal = true; 296 | } 297 | 298 | for (int i = 0; i < mTypeIds.length; i++) { 299 | String className = mStrings[mTypeIds[i].descriptorIdx]; 300 | 301 | if (className.length() == 1) { 302 | // primitive class 303 | mTypeIds[i].internal = true; 304 | } else if (className.charAt(0) == '[') { 305 | mTypeIds[i].internal = true; 306 | } 307 | 308 | //System.out.println(i + " " + 309 | // (mTypeIds[i].internal ? "INTERNAL" : "external") + " - " + 310 | // mStrings[mTypeIds[i].descriptorIdx]); 311 | } 312 | } 313 | 314 | 315 | /* 316 | * ======================================================================= 317 | * Queries 318 | * ======================================================================= 319 | */ 320 | 321 | /** 322 | * Returns the class name, given an index into the type_ids table. 323 | */ 324 | private String classNameFromTypeIndex(int idx) { 325 | return mStrings[mTypeIds[idx].descriptorIdx]; 326 | } 327 | 328 | /** 329 | * Returns an array of method argument type strings, given an index 330 | * into the proto_ids table. 331 | */ 332 | private String[] argArrayFromProtoIndex(int idx) { 333 | ProtoIdItem protoId = mProtoIds[idx]; 334 | String[] result = new String[protoId.types.length]; 335 | 336 | for (int i = 0; i < protoId.types.length; i++) { 337 | result[i] = mStrings[mTypeIds[protoId.types[i]].descriptorIdx]; 338 | } 339 | 340 | return result; 341 | } 342 | 343 | /** 344 | * Returns a string representing the method's return type, given an 345 | * index into the proto_ids table. 346 | */ 347 | private String returnTypeFromProtoIndex(int idx) { 348 | ProtoIdItem protoId = mProtoIds[idx]; 349 | return mStrings[mTypeIds[protoId.returnTypeIdx].descriptorIdx]; 350 | } 351 | 352 | /** 353 | * Returns an array with all of the class references that don't 354 | * correspond to classes in the DEX file. Each class reference has 355 | * a list of the referenced fields and methods associated with 356 | * that class. 357 | */ 358 | public ClassRef[] getExternalReferences() { 359 | // create a sparse array of ClassRef that parallels mTypeIds 360 | ClassRef[] sparseRefs = new ClassRef[mTypeIds.length]; 361 | 362 | // create entries for all externally-referenced classes 363 | int count = 0; 364 | for (int i = 0; i < mTypeIds.length; i++) { 365 | if (!mTypeIds[i].internal) { 366 | sparseRefs[i] = 367 | new ClassRef(mStrings[mTypeIds[i].descriptorIdx]); 368 | count++; 369 | } 370 | } 371 | 372 | // add fields and methods to the appropriate class entry 373 | addExternalFieldReferences(sparseRefs); 374 | addExternalMethodReferences(sparseRefs); 375 | 376 | // crunch out the sparseness 377 | ClassRef[] classRefs = new ClassRef[count]; 378 | int idx = 0; 379 | for (int i = 0; i < mTypeIds.length; i++) { 380 | if (sparseRefs[i] != null) 381 | classRefs[idx++] = sparseRefs[i]; 382 | } 383 | 384 | assert idx == count; 385 | 386 | return classRefs; 387 | } 388 | 389 | /** 390 | * Runs through the list of field references, inserting external 391 | * references into the appropriate ClassRef. 392 | */ 393 | private void addExternalFieldReferences(ClassRef[] sparseRefs) { 394 | for (int i = 0; i < mFieldIds.length; i++) { 395 | if (!mTypeIds[mFieldIds[i].classIdx].internal) { 396 | FieldIdItem fieldId = mFieldIds[i]; 397 | FieldRef newFieldRef = new FieldRef( 398 | classNameFromTypeIndex(fieldId.classIdx), 399 | classNameFromTypeIndex(fieldId.typeIdx), 400 | mStrings[fieldId.nameIdx]); 401 | sparseRefs[mFieldIds[i].classIdx].addField(newFieldRef); 402 | } 403 | } 404 | } 405 | 406 | /** 407 | * Runs through the list of method references, inserting external 408 | * references into the appropriate ClassRef. 409 | */ 410 | private void addExternalMethodReferences(ClassRef[] sparseRefs) { 411 | for (int i = 0; i < mMethodIds.length; i++) { 412 | if (!mTypeIds[mMethodIds[i].classIdx].internal) { 413 | MethodIdItem methodId = mMethodIds[i]; 414 | MethodRef newMethodRef = new MethodRef( 415 | classNameFromTypeIndex(methodId.classIdx), 416 | argArrayFromProtoIndex(methodId.protoIdx), 417 | returnTypeFromProtoIndex(methodId.protoIdx), 418 | mStrings[methodId.nameIdx]); 419 | sparseRefs[mMethodIds[i].classIdx].addMethod(newMethodRef); 420 | } 421 | } 422 | } 423 | 424 | /* 425 | * BEGIN MODIFIED SECTION 426 | */ 427 | 428 | /** 429 | * Returns the list of all method references. 430 | * @return method refs 431 | */ 432 | public MethodRef[] getMethodRefs() { 433 | MethodRef[] methodRefs = new MethodRef[mMethodIds.length]; 434 | for (int i = 0; i < mMethodIds.length; i++) { 435 | MethodIdItem methodId = mMethodIds[i]; 436 | methodRefs[i] = new MethodRef( 437 | classNameFromTypeIndex(methodId.classIdx), 438 | argArrayFromProtoIndex(methodId.protoIdx), 439 | returnTypeFromProtoIndex(methodId.protoIdx), 440 | mStrings[methodId.nameIdx]); 441 | } 442 | return methodRefs; 443 | } 444 | 445 | public FieldRef[] getFieldRefs() { 446 | FieldRef[] fieldRefs = new FieldRef[mFieldIds.length]; 447 | for (int i = 0; i < mFieldIds.length; i++) { 448 | FieldIdItem fieldId = mFieldIds[i]; 449 | fieldRefs[i] = new FieldRef( 450 | classNameFromTypeIndex(fieldId.classIdx), 451 | classNameFromTypeIndex(fieldId.typeIdx), 452 | mStrings[fieldId.nameIdx]); 453 | } 454 | return fieldRefs; 455 | } 456 | 457 | /* 458 | * END MODIFIED SECTION 459 | */ 460 | 461 | 462 | 463 | /* 464 | * ======================================================================= 465 | * Basic I/O functions 466 | * ======================================================================= 467 | */ 468 | 469 | /** 470 | * Seeks the DEX file to the specified absolute position. 471 | */ 472 | void seek(int position) throws IOException { 473 | mDexFile.seek(position); 474 | } 475 | 476 | /** 477 | * Fills the buffer by reading bytes from the DEX file. 478 | */ 479 | void readBytes(byte[] buffer) throws IOException { 480 | mDexFile.readFully(buffer); 481 | } 482 | 483 | /** 484 | * Reads a single signed byte value. 485 | */ 486 | byte readByte() throws IOException { 487 | mDexFile.readFully(tmpBuf, 0, 1); 488 | return tmpBuf[0]; 489 | } 490 | 491 | /** 492 | * Reads a signed 16-bit integer, byte-swapping if necessary. 493 | */ 494 | short readShort() throws IOException { 495 | mDexFile.readFully(tmpBuf, 0, 2); 496 | if (isBigEndian) { 497 | return (short) ((tmpBuf[1] & 0xff) | ((tmpBuf[0] & 0xff) << 8)); 498 | } else { 499 | return (short) ((tmpBuf[0] & 0xff) | ((tmpBuf[1] & 0xff) << 8)); 500 | } 501 | } 502 | 503 | /** 504 | * Reads a signed 32-bit integer, byte-swapping if necessary. 505 | */ 506 | int readInt() throws IOException { 507 | mDexFile.readFully(tmpBuf, 0, 4); 508 | 509 | if (isBigEndian) { 510 | return (tmpBuf[3] & 0xff) | ((tmpBuf[2] & 0xff) << 8) | 511 | ((tmpBuf[1] & 0xff) << 16) | ((tmpBuf[0] & 0xff) << 24); 512 | } else { 513 | return (tmpBuf[0] & 0xff) | ((tmpBuf[1] & 0xff) << 8) | 514 | ((tmpBuf[2] & 0xff) << 16) | ((tmpBuf[3] & 0xff) << 24); 515 | } 516 | } 517 | 518 | /** 519 | * Reads a variable-length unsigned LEB128 value. Does not attempt to 520 | * verify that the value is valid. 521 | * 522 | * @throws java.io.EOFException if we run off the end of the file 523 | */ 524 | int readUnsignedLeb128() throws IOException { 525 | int result = 0; 526 | byte val; 527 | 528 | do { 529 | val = readByte(); 530 | result = (result << 7) | (val & 0x7f); 531 | } while (val < 0); 532 | 533 | return result; 534 | } 535 | 536 | /** 537 | * Reads a UTF-8 string. 538 | * 539 | * We don't know how long the UTF-8 string is, so we have to read one 540 | * byte at a time. We could make an educated guess based on the 541 | * utf16_size and seek back if we get it wrong, but seeking backward 542 | * may cause the underlying implementation to reload I/O buffers. 543 | */ 544 | String readString() throws IOException { 545 | int utf16len = readUnsignedLeb128(); 546 | byte inBuf[] = new byte[utf16len * 3]; // worst case 547 | int idx; 548 | 549 | for (idx = 0; idx < inBuf.length; idx++) { 550 | byte val = readByte(); 551 | if (val == 0) 552 | break; 553 | inBuf[idx] = val; 554 | } 555 | 556 | return new String(inBuf, 0, idx, "UTF-8"); 557 | } 558 | 559 | 560 | /* 561 | * ======================================================================= 562 | * Internal "structure" declarations 563 | * ======================================================================= 564 | */ 565 | 566 | /** 567 | * Holds the contents of a header_item. 568 | */ 569 | static class HeaderItem { 570 | public int fileSize; 571 | public int headerSize; 572 | public int endianTag; 573 | public int stringIdsSize, stringIdsOff; 574 | public int typeIdsSize, typeIdsOff; 575 | public int protoIdsSize, protoIdsOff; 576 | public int fieldIdsSize, fieldIdsOff; 577 | public int methodIdsSize, methodIdsOff; 578 | public int classDefsSize, classDefsOff; 579 | 580 | /* expected magic values */ 581 | public static final byte[] DEX_FILE_MAGIC_v035 = 582 | "dex\n035\0".getBytes(StandardCharsets.US_ASCII); 583 | 584 | // Dex version 036 skipped because of an old dalvik bug on some versions 585 | // of android where dex files with that version number would erroneously 586 | // be accepted and run. See: art/runtime/dex_file.cc 587 | 588 | // V037 was introduced in API LEVEL 24 589 | public static final byte[] DEX_FILE_MAGIC_v037 = 590 | "dex\n037\0".getBytes(StandardCharsets.US_ASCII); 591 | public static final int ENDIAN_CONSTANT = 0x12345678; 592 | public static final int REVERSE_ENDIAN_CONSTANT = 0x78563412; 593 | } 594 | 595 | /** 596 | * Holds the contents of a type_id_item. 597 | * 598 | * This is chiefly a list of indices into the string table. We need 599 | * some additional bits of data, such as whether or not the type ID 600 | * represents a class defined in this DEX, so we use an object for 601 | * each instead of a simple integer. (Could use a parallel array, but 602 | * since this is a desktop app it's not essential.) 603 | */ 604 | static class TypeIdItem { 605 | public int descriptorIdx; // index into string_ids 606 | 607 | public boolean internal; // defined within this DEX file? 608 | } 609 | 610 | /** 611 | * Holds the contents of a proto_id_item. 612 | */ 613 | static class ProtoIdItem { 614 | public int shortyIdx; // index into string_ids 615 | public int returnTypeIdx; // index into type_ids 616 | public int parametersOff; // file offset to a type_list 617 | 618 | public int types[]; // contents of type list 619 | } 620 | 621 | /** 622 | * Holds the contents of a field_id_item. 623 | */ 624 | static class FieldIdItem { 625 | public int classIdx; // index into type_ids (defining class) 626 | public int typeIdx; // index into type_ids (field type) 627 | public int nameIdx; // index into string_ids 628 | } 629 | 630 | /** 631 | * Holds the contents of a method_id_item. 632 | */ 633 | static class MethodIdItem { 634 | public int classIdx; // index into type_ids 635 | public int protoIdx; // index into proto_ids 636 | public int nameIdx; // index into string_ids 637 | } 638 | 639 | /** 640 | * Holds the contents of a class_def_item. 641 | * 642 | * We don't really need a class for this, but there's some stuff in 643 | * the class_def_item that we might want later. 644 | */ 645 | static class ClassDefItem { 646 | public int classIdx; // index into type_ids 647 | } 648 | } 649 | -------------------------------------------------------------------------------- /src/main/java/com/android/dexdeps/DexDataException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.dexdeps; 18 | 19 | /** 20 | * Bad data found inside a DEX file. 21 | */ 22 | public class DexDataException extends RuntimeException { 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/android/dexdeps/FieldRef.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009 The Android Open Source Project 3 | * Copyright (C) 2015-2017 Keepsafe Software 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package com.android.dexdeps; 19 | 20 | public class FieldRef implements HasDeclaringClass { 21 | private String mDeclClass, mFieldType, mFieldName; 22 | 23 | /** 24 | * Initializes a new field reference. 25 | */ 26 | public FieldRef(String declClass, String fieldType, String fieldName) { 27 | mDeclClass = declClass; 28 | mFieldType = fieldType; 29 | mFieldName = fieldName; 30 | } 31 | 32 | /** 33 | * Gets the name of the field's declaring class. 34 | */ 35 | @Override 36 | public String getDeclClassName() { 37 | return mDeclClass; 38 | } 39 | 40 | /** 41 | * Gets the type name. Examples: "Ljava/lang/String;", "[I". 42 | */ 43 | public String getTypeName() { 44 | return mFieldType; 45 | } 46 | 47 | /** 48 | * Gets the field name. 49 | */ 50 | public String getName() { 51 | return mFieldName; 52 | } 53 | 54 | /* 55 | * BEGIN MODIFICATIONS 56 | */ 57 | 58 | @Override 59 | public boolean equals(Object obj) { 60 | if (!(obj instanceof FieldRef)) { 61 | return false; 62 | } 63 | FieldRef that = (FieldRef) obj; 64 | return this.mDeclClass.equals(that.mDeclClass) 65 | && this.mFieldName.equals(that.mFieldName) 66 | && this.mFieldType.equals(that.mFieldType); 67 | } 68 | 69 | @Override 70 | public int hashCode() { 71 | return mDeclClass.hashCode() 72 | ^ mFieldName.hashCode() 73 | ^ mFieldType.hashCode(); 74 | } 75 | 76 | /* 77 | * END MODIFICATIONS 78 | */ 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/android/dexdeps/HasDeclaringClass.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015-2016 KeepSafe Software 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.dexdeps; 18 | 19 | public interface HasDeclaringClass { 20 | String getDeclClassName(); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/android/dexdeps/MethodRef.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009 The Android Open Source Project 3 | * Copyright (C) 2015-2017 Keepsafe Software 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package com.android.dexdeps; 19 | 20 | import java.util.Arrays; 21 | 22 | public class MethodRef implements HasDeclaringClass { 23 | private String mDeclClass, mReturnType, mMethodName; 24 | private String[] mArgTypes; 25 | 26 | /** 27 | * Initializes a new field reference. 28 | */ 29 | public MethodRef(String declClass, String[] argTypes, String returnType, 30 | String methodName) { 31 | mDeclClass = declClass; 32 | mArgTypes = argTypes; 33 | mReturnType = returnType; 34 | mMethodName = methodName; 35 | } 36 | 37 | /** 38 | * Gets the name of the method's declaring class. 39 | */ 40 | @Override 41 | public String getDeclClassName() { 42 | return mDeclClass; 43 | } 44 | 45 | /** 46 | * Gets the method's descriptor. 47 | */ 48 | public String getDescriptor() { 49 | return descriptorFromProtoArray(mArgTypes, mReturnType); 50 | } 51 | 52 | /** 53 | * Gets the method's name. 54 | */ 55 | public String getName() { 56 | return mMethodName; 57 | } 58 | 59 | /** 60 | * Gets an array of method argument types. 61 | */ 62 | public String[] getArgumentTypeNames() { 63 | return mArgTypes; 64 | } 65 | 66 | /** 67 | * Gets the method's return type. Examples: "Ljava/lang/String;", "[I". 68 | */ 69 | public String getReturnTypeName() { 70 | return mReturnType; 71 | } 72 | 73 | /** 74 | * Returns the method descriptor, given the argument and return type 75 | * prototype strings. 76 | */ 77 | private static String descriptorFromProtoArray(String[] protos, 78 | String returnType) { 79 | StringBuilder builder = new StringBuilder(); 80 | 81 | builder.append("("); 82 | for (int i = 0; i < protos.length; i++) { 83 | builder.append(protos[i]); 84 | } 85 | 86 | builder.append(")"); 87 | builder.append(returnType); 88 | 89 | return builder.toString(); 90 | } 91 | 92 | /* 93 | * BEGIN MODIFICATION 94 | */ 95 | 96 | @Override public boolean equals(Object o) { 97 | if (!(o instanceof MethodRef)) { 98 | return false; 99 | } 100 | MethodRef other = (MethodRef) o; 101 | return other.mDeclClass.equals(mDeclClass) && 102 | other.mReturnType.equals(mReturnType) && 103 | other.mMethodName.equals(mMethodName) && 104 | Arrays.equals(other.mArgTypes, mArgTypes); 105 | } 106 | 107 | @Override public int hashCode() { 108 | return mDeclClass.hashCode() ^ mReturnType.hashCode() ^ 109 | mMethodName.hashCode() ^ Arrays.hashCode(mArgTypes); 110 | } 111 | 112 | /* 113 | * END MODIFICATION 114 | */ 115 | } 116 | -------------------------------------------------------------------------------- /src/main/java/com/android/dexdeps/Output.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009 The Android Open Source Project 3 | * Copyright (C) 2017 Keepsafe Software 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package com.android.dexdeps; 19 | 20 | import java.io.PrintStream; 21 | 22 | /** 23 | * Generate fancy output. 24 | */ 25 | public class Output { 26 | private static final String IN0 = ""; 27 | private static final String IN1 = " "; 28 | private static final String IN2 = " "; 29 | private static final String IN3 = " "; 30 | private static final String IN4 = " "; 31 | 32 | private static final PrintStream out = System.out; 33 | 34 | private static void generateHeader0(String fileName, String format) { 35 | if (format.equals("brief")) { 36 | if (fileName != null) { 37 | out.println("File: " + fileName); 38 | } 39 | } else if (format.equals("xml")) { 40 | if (fileName != null) { 41 | out.println(IN0 + ""); 42 | } else { 43 | out.println(IN0 + ""); 44 | } 45 | } else { 46 | /* should've been trapped in arg handler */ 47 | throw new RuntimeException("unknown output format"); 48 | } 49 | } 50 | 51 | public static void generateFirstHeader(String fileName, String format) { 52 | generateHeader0(fileName, format); 53 | } 54 | 55 | public static void generateHeader(String fileName, String format) { 56 | out.println(); 57 | generateHeader0(fileName, format); 58 | } 59 | 60 | public static void generateFooter(String format) { 61 | if (format.equals("brief")) { 62 | // Nothing to do. 63 | } else if (format.equals("xml")) { 64 | out.println(""); 65 | } else { 66 | /* should've been trapped in arg handler */ 67 | throw new RuntimeException("unknown output format"); 68 | } 69 | } 70 | 71 | public static void generate(DexData dexData, String format, 72 | boolean justClasses) { 73 | if (format.equals("brief")) { 74 | printBrief(dexData, justClasses); 75 | } else if (format.equals("xml")) { 76 | printXml(dexData, justClasses); 77 | } else { 78 | /* should've been trapped in arg handler */ 79 | throw new RuntimeException("unknown output format"); 80 | } 81 | } 82 | 83 | /** 84 | * Prints the data in a simple human-readable format. 85 | */ 86 | static void printBrief(DexData dexData, boolean justClasses) { 87 | ClassRef[] externClassRefs = dexData.getExternalReferences(); 88 | 89 | printClassRefs(externClassRefs, justClasses); 90 | 91 | if (!justClasses) { 92 | printFieldRefs(externClassRefs); 93 | printMethodRefs(externClassRefs); 94 | } 95 | } 96 | 97 | /** 98 | * Prints the list of classes in a simple human-readable format. 99 | */ 100 | static void printClassRefs(ClassRef[] classes, boolean justClasses) { 101 | if (!justClasses) { 102 | out.println("Classes:"); 103 | } 104 | 105 | for (int i = 0; i < classes.length; i++) { 106 | ClassRef ref = classes[i]; 107 | 108 | out.println(descriptorToDot(ref.getName())); 109 | } 110 | } 111 | 112 | /** 113 | * Prints the list of fields in a simple human-readable format. 114 | */ 115 | static void printFieldRefs(ClassRef[] classes) { 116 | out.println("\nFields:"); 117 | for (int i = 0; i < classes.length; i++) { 118 | FieldRef[] fields = classes[i].getFieldArray(); 119 | 120 | for (int j = 0; j < fields.length; j++) { 121 | FieldRef ref = fields[j]; 122 | 123 | out.println(descriptorToDot(ref.getDeclClassName()) + 124 | "." + ref.getName() + " : " + ref.getTypeName()); 125 | } 126 | } 127 | } 128 | 129 | /** 130 | * Prints the list of methods in a simple human-readable format. 131 | */ 132 | static void printMethodRefs(ClassRef[] classes) { 133 | out.println("\nMethods:"); 134 | for (int i = 0; i < classes.length; i++) { 135 | MethodRef[] methods = classes[i].getMethodArray(); 136 | 137 | for (int j = 0; j < methods.length; j++) { 138 | MethodRef ref = methods[j]; 139 | 140 | out.println(descriptorToDot(ref.getDeclClassName()) + 141 | "." + ref.getName() + " : " + ref.getDescriptor()); 142 | } 143 | } 144 | } 145 | 146 | /** 147 | * Prints the output in XML format. 148 | * 149 | * We shouldn't need to XML-escape the field/method info. 150 | */ 151 | static void printXml(DexData dexData, boolean justClasses) { 152 | ClassRef[] externClassRefs = dexData.getExternalReferences(); 153 | 154 | /* 155 | * Iterate through externClassRefs. For each class, dump all of 156 | * the matching fields and methods. 157 | */ 158 | String prevPackage = null; 159 | for (int i = 0; i < externClassRefs.length; i++) { 160 | ClassRef cref = externClassRefs[i]; 161 | String declClassName = cref.getName(); 162 | String className = classNameOnly(declClassName); 163 | String packageName = packageNameOnly(declClassName); 164 | 165 | /* 166 | * If we're in a different package, emit the appropriate tags. 167 | */ 168 | if (!packageName.equals(prevPackage)) { 169 | if (prevPackage != null) { 170 | out.println(IN1 + ""); 171 | } 172 | 173 | out.println(IN1 + 174 | ""); 175 | 176 | prevPackage = packageName; 177 | } 178 | 179 | out.println(IN2 + ""); 180 | if (!justClasses) { 181 | printXmlFields(cref); 182 | printXmlMethods(cref); 183 | } 184 | out.println(IN2 + ""); 185 | } 186 | 187 | if (prevPackage != null) 188 | out.println(IN1 + ""); 189 | } 190 | 191 | /** 192 | * Prints the externally-visible fields in XML format. 193 | */ 194 | private static void printXmlFields(ClassRef cref) { 195 | FieldRef[] fields = cref.getFieldArray(); 196 | for (int i = 0; i < fields.length; i++) { 197 | FieldRef fref = fields[i]; 198 | 199 | out.println(IN3 + ""); 201 | } 202 | } 203 | 204 | /** 205 | * Prints the externally-visible methods in XML format. 206 | */ 207 | private static void printXmlMethods(ClassRef cref) { 208 | MethodRef[] methods = cref.getMethodArray(); 209 | for (int i = 0; i < methods.length; i++) { 210 | MethodRef mref = methods[i]; 211 | String declClassName = mref.getDeclClassName(); 212 | boolean constructor; 213 | 214 | constructor = mref.getName().equals(""); 215 | if (constructor) { 216 | // use class name instead of method name 217 | out.println(IN3 + ""); 219 | } else { 220 | out.println(IN3 + ""); 223 | } 224 | String[] args = mref.getArgumentTypeNames(); 225 | for (int j = 0; j < args.length; j++) { 226 | out.println(IN4 + ""); 228 | } 229 | if (constructor) { 230 | out.println(IN3 + ""); 231 | } else { 232 | out.println(IN3 + ""); 233 | } 234 | } 235 | } 236 | 237 | 238 | /* 239 | * ======================================================================= 240 | * Utility functions 241 | * ======================================================================= 242 | */ 243 | 244 | /* 245 | * MODIFICATIONS: 246 | * primitiveTypeLabel, descriptorToDot both made public. 247 | */ 248 | 249 | /** 250 | * Converts a single-character primitive type into its human-readable 251 | * equivalent. 252 | */ 253 | public static String primitiveTypeLabel(char typeChar) { 254 | /* primitive type; substitute human-readable name in */ 255 | switch (typeChar) { 256 | case 'B': return "byte"; 257 | case 'C': return "char"; 258 | case 'D': return "double"; 259 | case 'F': return "float"; 260 | case 'I': return "int"; 261 | case 'J': return "long"; 262 | case 'S': return "short"; 263 | case 'V': return "void"; 264 | case 'Z': return "boolean"; 265 | default: 266 | /* huh? */ 267 | System.err.println("Unexpected class char " + typeChar); 268 | assert false; 269 | return "UNKNOWN"; 270 | } 271 | } 272 | 273 | /** 274 | * Converts a type descriptor to human-readable "dotted" form. For 275 | * example, "Ljava/lang/String;" becomes "java.lang.String", and 276 | * "[I" becomes "int[]. 277 | */ 278 | public static String descriptorToDot(String descr) { 279 | int targetLen = descr.length(); 280 | int offset = 0; 281 | int arrayDepth = 0; 282 | 283 | /* strip leading [s; will be added to end */ 284 | while (targetLen > 1 && descr.charAt(offset) == '[') { 285 | offset++; 286 | targetLen--; 287 | } 288 | arrayDepth = offset; 289 | 290 | if (targetLen == 1) { 291 | descr = primitiveTypeLabel(descr.charAt(offset)); 292 | offset = 0; 293 | targetLen = descr.length(); 294 | } else { 295 | /* account for leading 'L' and trailing ';' */ 296 | if (targetLen >= 2 && descr.charAt(offset) == 'L' && 297 | descr.charAt(offset+targetLen-1) == ';') 298 | { 299 | targetLen -= 2; /* two fewer chars to copy */ 300 | offset++; /* skip the 'L' */ 301 | } 302 | } 303 | 304 | char[] buf = new char[targetLen + arrayDepth * 2]; 305 | 306 | /* copy class name over */ 307 | int i; 308 | for (i = 0; i < targetLen; i++) { 309 | char ch = descr.charAt(offset + i); 310 | buf[i] = (ch == '/') ? '.' : ch; 311 | } 312 | 313 | /* add the appopriate number of brackets for arrays */ 314 | while (arrayDepth-- > 0) { 315 | buf[i++] = '['; 316 | buf[i++] = ']'; 317 | } 318 | assert i == buf.length; 319 | 320 | return new String(buf); 321 | } 322 | 323 | /** 324 | * Extracts the class name from a type descriptor. 325 | */ 326 | public static String classNameOnly(String typeName) { 327 | String dotted = descriptorToDot(typeName); 328 | 329 | int start = dotted.lastIndexOf("."); 330 | if (start < 0) { 331 | return dotted; 332 | } else { 333 | return dotted.substring(start+1); 334 | } 335 | } 336 | 337 | /** 338 | * Extracts the package name from a type descriptor, and returns it in 339 | * dotted form. 340 | */ 341 | public static String packageNameOnly(String typeName) { 342 | String dotted = descriptorToDot(typeName); 343 | 344 | int end = dotted.lastIndexOf("."); 345 | if (end < 0) { 346 | /* lives in default package */ 347 | return ""; 348 | } else { 349 | return dotted.substring(0, end); 350 | } 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /src/main/kotlin/au/com/timmutton/redexplugin/DexFile.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015-2016 KeepSafe Software 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package au.com.timmutton.redexplugin 18 | 19 | import com.android.dexdeps.DexData 20 | import java.io.File 21 | import java.io.RandomAccessFile 22 | import java.util.* 23 | import java.util.Collections.emptyList 24 | import java.util.zip.ZipEntry 25 | import java.util.zip.ZipException 26 | import java.util.zip.ZipFile 27 | 28 | /** 29 | * A physical file and the {@link DexData} contained therein. 30 | * 31 | * A DexFile contains an open file, possibly a temp file. When consumers are 32 | * finished with the DexFile, it should be cleaned up with 33 | * {@link DexFile#dispose()}. 34 | */ 35 | class DexFile(val file: File, val isTemp: Boolean, val isInstantRun: Boolean = false) { 36 | val data: DexData 37 | val raf: RandomAccessFile = RandomAccessFile(file, "r") 38 | 39 | init { 40 | data = DexData(raf) 41 | data.load() 42 | } 43 | 44 | fun dispose() { 45 | raf.close() 46 | if (isTemp) { 47 | file.delete() 48 | } 49 | } 50 | 51 | companion object { 52 | /** 53 | * Extracts a list of {@link DexFile} instances from the given file. 54 | * 55 | * DexFiles can be extracted either from an Android APK file, or from a raw 56 | * {@code classes.dex} file. 57 | * 58 | * @param file the APK or dex file. 59 | * @return a list of DexFile objects representing data in the given file. 60 | */ 61 | fun extractDexData(file: File?): List { 62 | if (file == null || !file.exists()) { 63 | return emptyList() 64 | } 65 | 66 | try { 67 | return extractDexFromZip(file) 68 | } catch (e: ZipException) { 69 | // not a zip, no problem 70 | } 71 | 72 | return listOf(DexFile(file, false)) 73 | } 74 | 75 | /** 76 | * Attempts to unzip the file and extract all dex files inside of it. 77 | * 78 | * It is assumed that {@code file} is an APK file resulting from an Android 79 | * build, containing one or more appropriately-named classes.dex files. 80 | * 81 | * @param file the APK file from which to extract dex data. 82 | * @return a list of contained dex files. 83 | * @throws ZipException if {@code file} is not a zip file. 84 | */ 85 | fun extractDexFromZip(file: File): List = ZipFile(file).use { zipfile -> 86 | val entries = zipfile.entries().toList() 87 | 88 | val mainDexFiles = entries.filter { it.name.matches(Regex("classes.*\\.dex")) }.map { entry -> 89 | val temp = File.createTempFile("dexcount", ".dex") 90 | temp.deleteOnExit() 91 | 92 | zipfile.getInputStream(entry).use { input -> 93 | IOUtil.drainToFile(input, temp) 94 | } 95 | 96 | DexFile(temp, true) 97 | }.toMutableList() 98 | 99 | mainDexFiles.addAll(extractIncrementalDexFiles(zipfile, entries)) 100 | 101 | return mainDexFiles 102 | } 103 | 104 | /** 105 | * Attempts to extract dex files embedded in a nested instant-run.zip file 106 | * produced by Android Studio 2.0. If present, such files are extracted to 107 | * temporary files on disk and returned as a list. If not, an empty mutable 108 | * list is returned. 109 | * 110 | * @param apk the APK file from which to extract dex data. 111 | * @param zipEntries a list of ZipEntry objects inside of the APK. 112 | * @return a list, possibly empty, of instant-run dex data. 113 | */ 114 | fun extractIncrementalDexFiles(apk: ZipFile, zipEntries: List): List { 115 | val incremental = zipEntries.filter { (it.name == "instant-run.zip") } 116 | if (incremental.size != 1) { 117 | return emptyList() 118 | } 119 | 120 | val instantRunFile = File.createTempFile("instant-run", ".zip") 121 | instantRunFile.deleteOnExit() 122 | 123 | apk.getInputStream(incremental[0]).use { input -> 124 | IOUtil.drainToFile(input, instantRunFile) 125 | } 126 | 127 | ZipFile(instantRunFile).use { instantRunZip -> 128 | val entries = Collections.list(instantRunZip.entries()) 129 | val dexEntries = entries.filter { it.name.endsWith(".dex") } 130 | 131 | return dexEntries.map { entry -> 132 | val temp = File.createTempFile("dexcount", ".dex") 133 | temp.deleteOnExit() 134 | 135 | instantRunZip.getInputStream(entry).use { input -> 136 | IOUtil.drainToFile(input, temp) 137 | } 138 | 139 | DexFile(temp, true, true) 140 | } 141 | } 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/main/kotlin/au/com/timmutton/redexplugin/IOUtil.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 KeepSafe Software 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package au.com.timmutton.redexplugin 18 | 19 | import java.io.File 20 | import java.io.InputStream 21 | 22 | object IOUtil { 23 | @JvmStatic fun drainToFile(stream: InputStream, file: File) { 24 | stream.use { input -> 25 | File(file.path).outputStream().use { output -> 26 | input.copyTo(output) 27 | output.flush() 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/kotlin/au/com/timmutton/redexplugin/RedexDownloadTask.kt: -------------------------------------------------------------------------------- 1 | package au.com.timmutton.redexplugin 2 | 3 | import java.util.regex.Pattern 4 | import java.util.regex.Matcher 5 | import java.nio.file.Files 6 | import java.nio.charset.Charset 7 | import java.io.File 8 | import java.io.FileOutputStream 9 | import java.io.FileNotFoundException 10 | import java.io.IOException 11 | import java.io.ByteArrayOutputStream 12 | import com.github.kittinunf.fuel.Fuel 13 | import com.google.gson.JsonObject 14 | import com.google.gson.JsonParser 15 | import com.google.gson.JsonElement 16 | import org.apache.tools.ant.taskdefs.condition.Os 17 | import org.gradle.api.DefaultTask 18 | import org.gradle.api.tasks.TaskAction 19 | 20 | 21 | open class RedexDownloadTask : DefaultTask() { 22 | var requestedVersion : String? = "latest" 23 | public val buildDir = project.buildDir.getPath() 24 | 25 | // Returns the location where redex will be downloaded 26 | // after this task is run 27 | public fun initialise(ext : RedexExtension) : File? { 28 | requestedVersion = ext.version 29 | val (redex, _) = getRedexExecutableFile() 30 | return redex 31 | } 32 | 33 | @TaskAction 34 | // download redex from the github release page 35 | public fun run() { 36 | if (requestedVersion == null) { 37 | return 38 | } 39 | val (redex, url) = getRedexExecutableFile() 40 | if (redex == null || url == null) { 41 | return 42 | } 43 | if (!redex.exists()) { 44 | // Only download if we don't already have it 45 | downloadFile(url, redex) 46 | redex.setExecutable(true) 47 | } 48 | } 49 | 50 | private fun downloadFile(url : String, dest : File) { 51 | val (_,_, result) = Fuel.get(url).response() 52 | val (data : ByteArray?, error) = result 53 | if (error != null) { 54 | throw error 55 | } 56 | if (data == null) { 57 | throw IOException("No data from download") 58 | } 59 | dest.writeBytes(data) 60 | } 61 | 62 | private fun downloadJson(url : String) : JsonElement { 63 | val (_,_, result) = Fuel.get(url).responseString() 64 | val (data : String?, error) = result 65 | if (error != null) { 66 | throw error 67 | } 68 | if (data == null) { 69 | throw IOException("No data from download") 70 | } 71 | return JsonParser().parse(data) 72 | } 73 | 74 | // Returns the file where the redex binary will be placed 75 | // and the URL where it will be downloaded from 76 | // 77 | // If we're on an unsupported platform, print to stderr and fallback 78 | // to finding redex on the PATH 79 | fun getRedexExecutableFile() : Pair { 80 | val tag = if (requestedVersion == "latest") getLatestRedexTag() 81 | else requestedVersion 82 | val os = getOS() 83 | if (os != null) { 84 | val redex_exec = "redex_$os" 85 | val url = "https://github.com/facebook/redex/releases/download/$tag/$redex_exec" 86 | return Pair(File("$buildDir/${redex_exec}_$tag"), url) 87 | } 88 | return Pair(null, null) 89 | } 90 | 91 | // Use the github api to find the tag name of the most recent release 92 | private fun getLatestRedexTag() : String { 93 | val releasesUrl = "https://api.github.com/repos/facebook/redex/releases" 94 | val releases = downloadJson(releasesUrl) 95 | 96 | // more recent releases are near the beginning 97 | val latest = releases.getAsJsonArray().get(0).getAsJsonObject() 98 | return latest.get("tag_name").getAsString() 99 | } 100 | 101 | // get a string like _ of the current machine 102 | fun getOS() : String? { 103 | if (Os.isFamily(Os.FAMILY_UNIX)) { 104 | // TODO support other architectures (32bit, non x86, etc.) 105 | return "linux_x86_64" 106 | } else if (Os.isFamily(Os.FAMILY_MAC)) { 107 | return "macos_x86_64" 108 | } 109 | System.err.println( 110 | "Your platform isn't supported (yet) for downloading redex." + 111 | "Please follow instructions at https://github.com/facebok/redex." + 112 | "Assuming redex is in your PATH") 113 | return null 114 | } 115 | } 116 | 117 | -------------------------------------------------------------------------------- /src/main/kotlin/au/com/timmutton/redexplugin/RedexExtension.kt: -------------------------------------------------------------------------------- 1 | package au.com.timmutton.redexplugin 2 | 3 | import com.android.build.gradle.AppExtension 4 | import java.io.File 5 | 6 | /** 7 | * @author timmutton 8 | */ 9 | open class RedexExtension(appExtension: AppExtension) { 10 | var configFile : File? = null 11 | var proguardConfigFiles : List? = null 12 | var proguardMapFile : File? = null 13 | var jarFiles : List? = null 14 | var keepFile : File? = null 15 | var otherArgs : String? = null 16 | var passes : List? = null 17 | var showStats: Boolean = true 18 | 19 | val sdkDirectory: File? = appExtension.sdkDirectory 20 | 21 | // null means don't download 22 | // latest means the most recent redex release 23 | // any other string is a tag name in 24 | // github.com/facebook/redex/releases/tag/ 25 | var version : String? = "latest" 26 | } 27 | -------------------------------------------------------------------------------- /src/main/kotlin/au/com/timmutton/redexplugin/RedexPlugin.kt: -------------------------------------------------------------------------------- 1 | package au.com.timmutton.redexplugin 2 | 3 | import com.android.build.gradle.AppExtension 4 | import com.android.build.gradle.AppPlugin 5 | import java.io.File 6 | import org.gradle.api.Plugin 7 | import org.gradle.api.Project 8 | import org.gradle.api.tasks.StopExecutionException 9 | 10 | class RedexPlugin : Plugin { 11 | override fun apply(project: Project) { 12 | if (project.plugins.hasPlugin(AppPlugin::class.java)) { 13 | val android = project.extensions.getByType(AppExtension::class.java) 14 | val extension = project.extensions.create("redex", RedexExtension::class.java, android) 15 | 16 | project.afterEvaluate { 17 | var download : RedexDownloadTask? = null 18 | var redexPath : File? = null 19 | if (extension.version != null) { 20 | download = project.tasks.create("redexDownload", RedexDownloadTask::class.java) 21 | redexPath = download.initialise(extension) 22 | } 23 | 24 | android.applicationVariants.all { 25 | val task = project.tasks.create("redex${it.name.capitalize()}", RedexTask::class.java) 26 | task.initialise(it, extension, redexPath) 27 | if (download != null) { 28 | task.dependsOn(download) 29 | } 30 | } 31 | } 32 | } else { 33 | throw StopExecutionException("Redex requires the android application plugin") 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/kotlin/au/com/timmutton/redexplugin/RedexTask.kt: -------------------------------------------------------------------------------- 1 | package au.com.timmutton.redexplugin 2 | 3 | import au.com.timmutton.redexplugin.internal.RedexConfiguration 4 | import au.com.timmutton.redexplugin.internal.RedexConfigurationContainer 5 | import com.android.build.gradle.api.ApplicationVariant 6 | import com.android.builder.model.SigningConfig 7 | import com.google.gson.Gson 8 | import org.gradle.api.logging.LogLevel 9 | import org.gradle.api.tasks.Exec 10 | import org.gradle.api.tasks.InputFile 11 | import org.gradle.process.internal.ExecException 12 | import java.io.File 13 | import java.io.FileNotFoundException 14 | import java.io.FileWriter 15 | import java.lang.IllegalArgumentException 16 | import java.nio.file.Files 17 | import java.nio.file.StandardCopyOption 18 | 19 | /** 20 | * @author timmutton 21 | */ 22 | open class RedexTask: Exec() { 23 | private var signingConfig: SigningConfig? = null 24 | private var configFile : File? = null 25 | private var proguardConfigFiles : List? = null 26 | private var proguardMapFile : File? = null 27 | private var jarFiles : List? = null 28 | private var keepFile : File? = null 29 | private var otherArgs : String? = null 30 | private var passes: List? = null 31 | private var showStats: Boolean = true 32 | private var sdkDirectory: File? = null 33 | private var redex : String? = null 34 | 35 | @InputFile 36 | private lateinit var inputFile: File 37 | 38 | @Suppress("UNCHECKED_CAST") 39 | // Must use DSL to instantiate class, which means I cant pass variant as a constructor argument 40 | fun initialise(variant: ApplicationVariant, extension: RedexExtension, dlRedex : File?) { 41 | description = "Run Redex tool on your ${variant.name.capitalize()} apk" 42 | 43 | configFile = extension.configFile 44 | proguardConfigFiles = extension.proguardConfigFiles 45 | /*?: variant.let { 46 | val proguardFiles = it.buildType.proguardFiles.toMutableList() 47 | proguardFiles.addAll(it.mergedFlavor.proguardFiles) 48 | proguardFiles 49 | }*/ 50 | 51 | proguardMapFile = extension.proguardMapFile 52 | /*?: variant.mappingFile*/ 53 | jarFiles = extension.jarFiles 54 | keepFile = extension.keepFile 55 | /*?: variant.let { 56 | it.buildType.multiDexKeepProguard 57 | // TODO: add support for the merged flavor keep file 58 | // it.mergedFlavor.multiDexKeepProguard 59 | }*/ 60 | otherArgs = extension.otherArgs 61 | passes = extension.passes 62 | showStats = extension.showStats 63 | signingConfig = variant.buildType.signingConfig 64 | sdkDirectory = extension.sdkDirectory 65 | 66 | dependsOn(variant.assemble) 67 | mustRunAfter(variant.assemble) 68 | 69 | if(variant.buildType.isMinifyEnabled) { 70 | variant.assemble.finalizedBy(this) 71 | } 72 | 73 | val output = variant.outputs.first { it.outputFile.name.endsWith(".apk") } 74 | inputFile = output.outputFile 75 | 76 | redex = dlRedex?.absolutePath ?: "redex" 77 | 78 | if (passes != null && configFile != null) { 79 | throw IllegalArgumentException( 80 | "Cannot specify both passes and configFile"); 81 | } 82 | } 83 | 84 | override fun exec() { 85 | sdkDirectory?.let { 86 | environment("ANDROID_SDK", it) 87 | } 88 | 89 | passes?.let { 90 | val redexConfig = Gson().toJson(RedexConfigurationContainer(RedexConfiguration(it))) 91 | val config = File(project.buildDir, "redex.config") 92 | config.createNewFile() 93 | val writer = FileWriter(config) 94 | val configString = Gson().toJson(redexConfig) 95 | writer.write(configString.substring(1, configString.length - 1).replace("\\", "")) 96 | writer.close() 97 | args("-c", config.absolutePath) 98 | } 99 | 100 | configFile?.let { 101 | if(it.exists()) { 102 | args("-c", it.absolutePath) 103 | } 104 | } 105 | 106 | proguardConfigFiles?.forEach { 107 | if(it.exists()) { 108 | args("-P", it.absolutePath) 109 | } 110 | } 111 | 112 | proguardMapFile?.let { 113 | if(it.exists()) { 114 | args("-m", it.absolutePath) 115 | } 116 | } 117 | 118 | jarFiles?.forEach { 119 | if(it.exists()) { 120 | args("-j", it.absolutePath) 121 | } 122 | } 123 | 124 | keepFile?.let { 125 | if(it.exists()) { 126 | args("-k", it.absolutePath) 127 | } 128 | } 129 | 130 | otherArgs?.let { 131 | args("", it) 132 | } 133 | 134 | signingConfig?.let { 135 | args("--sign", 136 | "--keystore", it.storeFile.absolutePath, 137 | "--keyalias", it.keyAlias, 138 | "--keypass", it.keyPassword) 139 | } 140 | 141 | val outputFile = File(inputFile.toString()) 142 | 143 | val unredexed = File(inputFile.toString().replace(".apk", "-unredexed.apk")) 144 | Files.move(inputFile.toPath(), unredexed.toPath(), StandardCopyOption.REPLACE_EXISTING) 145 | inputFile = unredexed 146 | 147 | args("-o", "$outputFile", "$inputFile") 148 | executable(redex!!) 149 | 150 | try { 151 | super.exec() 152 | 153 | if(showStats) { 154 | logStats(outputFile) 155 | } 156 | } catch (e: ExecException) { 157 | if (e.message != null && e.message!!.contains("A problem occurred starting process")) { 158 | throw ExecException("A problem occurred starting Redex. " + 159 | "Ensure you have installed Redex using the instructions at https://github.com/facebook/redex") 160 | } else { 161 | throw e 162 | } 163 | } 164 | } 165 | 166 | private fun logStats(outputFile: File) { 167 | val originalDexData = DexFile.extractDexData(inputFile) 168 | val newDexData = DexFile.extractDexData(outputFile) 169 | 170 | try { 171 | val startingMethods = originalDexData.sumBy { it.data.methodRefs.size } 172 | val startingFields = originalDexData.sumBy { it.data.fieldRefs.size } 173 | val startingSize = inputFile.length().toInt() 174 | 175 | logger.log(LogLevel.LIFECYCLE, "\nBefore redex:") 176 | logger.log(LogLevel.LIFECYCLE, "\t$startingMethods methods") 177 | logger.log(LogLevel.LIFECYCLE, "\t$startingFields fields") 178 | logger.log(LogLevel.LIFECYCLE, "\t$startingSize bytes") 179 | 180 | val methods = newDexData.sumBy { it.data.methodRefs.size } 181 | val methodPercentage = "%.2f".format(methods.toFloat() / startingMethods * 100f) 182 | val fields = newDexData.sumBy { it.data.fieldRefs.size } 183 | val fieldPercentage = "%.2f".format(fields.toFloat() / startingFields * 100f) 184 | val size = outputFile.length().toInt() 185 | val sizePercentage = "%.2f".format(size.toFloat() / startingSize * 100f) 186 | 187 | logger.log(LogLevel.LIFECYCLE, "After redex:") 188 | logger.log(LogLevel.LIFECYCLE, "\t$methods methods (%$methodPercentage of original)") 189 | logger.log(LogLevel.LIFECYCLE, "\t$fields fields (%$fieldPercentage of original)") 190 | logger.log(LogLevel.LIFECYCLE, "\t$size bytes (%$sizePercentage of original)") 191 | } finally { 192 | originalDexData.forEach { it.dispose() } 193 | newDexData.forEach { it.dispose() } 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/main/kotlin/au/com/timmutton/redexplugin/internal/RedexConfiguration.kt: -------------------------------------------------------------------------------- 1 | package au.com.timmutton.redexplugin.internal 2 | 3 | /** 4 | * @author timmutton 5 | */ 6 | data class RedexConfiguration(val passes: List) -------------------------------------------------------------------------------- /src/main/kotlin/au/com/timmutton/redexplugin/internal/RedexConfigurationContainer.kt: -------------------------------------------------------------------------------- 1 | package au.com.timmutton.redexplugin.internal 2 | 3 | /** 4 | * @author timmutton 5 | */ 6 | data class RedexConfigurationContainer(val redex: RedexConfiguration) -------------------------------------------------------------------------------- /src/main/resources/META-INF/gradle-plugins/redex.properties: -------------------------------------------------------------------------------- 1 | implementation-class=au.com.timmutton.redexplugin.RedexPlugin --------------------------------------------------------------------------------