├── .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 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
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 |
81 |
82 |
83 |
84 |
85 |
86 | 1.8
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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
--------------------------------------------------------------------------------