├── plugin
├── .gitignore
├── src
│ ├── main
│ │ ├── resources
│ │ │ └── META-INF
│ │ │ │ └── gradle-plugins
│ │ │ │ └── com.mosect.smali.plugin.SmaliPlugin.properties
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── mosect
│ │ │ │ └── smali
│ │ │ │ └── plugin
│ │ │ │ ├── parser
│ │ │ │ ├── SmaliAnnotationNode.java
│ │ │ │ ├── SmaliNodeMatcher.java
│ │ │ │ ├── SmaliClassNode.java
│ │ │ │ ├── SmaliParseResult.java
│ │ │ │ ├── SmaliFieldNode.java
│ │ │ │ ├── SmaliEndNode.java
│ │ │ │ ├── SmaliParseError.java
│ │ │ │ ├── SmaliNode.java
│ │ │ │ ├── SmaliToken.java
│ │ │ │ ├── SmaliMethodNode.java
│ │ │ │ ├── SmaliBlockNode.java
│ │ │ │ └── SmaliParser.java
│ │ │ │ ├── util
│ │ │ │ ├── IOUtils.java
│ │ │ │ ├── RegexMatcher.java
│ │ │ │ └── TextUtils.java
│ │ │ │ └── dex
│ │ │ │ ├── SmaliException.java
│ │ │ │ ├── MethodNameMatcher.java
│ │ │ │ ├── DexDecoder.java
│ │ │ │ ├── SimpleSmaliParser.java
│ │ │ │ ├── CopyValueMatcher.java
│ │ │ │ ├── DexMaker.java
│ │ │ │ ├── MemberOperation.java
│ │ │ │ ├── ClassesSource.java
│ │ │ │ ├── ClassPosition.java
│ │ │ │ ├── DexHandler.java
│ │ │ │ ├── ClassOperation.java
│ │ │ │ └── SmaliMerger.java
│ │ └── groovy
│ │ │ └── com
│ │ │ └── mosect
│ │ │ └── smali
│ │ │ └── plugin
│ │ │ ├── SmaliExtension.groovy
│ │ │ └── SmaliPlugin.groovy
│ └── test
│ │ └── java
│ │ └── com
│ │ └── mosect
│ │ └── smali
│ │ └── plugin
│ │ └── MainTest.java
└── build.gradle
├── settings.gradle
├── .idea
├── .gitignore
├── compiler.xml
├── vcs.xml
├── misc.xml
├── gradle.xml
└── jarRepositories.xml
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── gradlew.bat
├── gradlew
└── README.md
/plugin/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':plugin'
2 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mosect/Android-SmaliPlugin/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/plugin/src/main/resources/META-INF/gradle-plugins/com.mosect.smali.plugin.SmaliPlugin.properties:
--------------------------------------------------------------------------------
1 | implementation-class = com.mosect.smali.plugin.SmaliPlugin
2 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/parser/SmaliAnnotationNode.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.parser;
2 |
3 | public class SmaliAnnotationNode extends SmaliBlockNode {
4 |
5 | @Override
6 | public SmaliAnnotationNode createEmpty() {
7 | return new SmaliAnnotationNode();
8 | }
9 |
10 | @Override
11 | public String getType() {
12 | return "annotation";
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/parser/SmaliNodeMatcher.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.parser;
2 |
3 | /**
4 | * 节点匹配器
5 | */
6 | public interface SmaliNodeMatcher {
7 |
8 | /**
9 | * 重置匹配器
10 | */
11 | void reset();
12 |
13 | /**
14 | * 节点匹配
15 | *
16 | * @param parent 父节点
17 | * @param node 节点
18 | * @param index 节点下标
19 | * @return 0,不匹配;1,匹配;2,匹配结束
20 | */
21 | int match(SmaliNode parent, SmaliNode node, int index);
22 | }
23 |
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/util/IOUtils.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.util;
2 |
3 | import java.io.File;
4 | import java.io.IOException;
5 |
6 | public final class IOUtils {
7 |
8 | private IOUtils() {
9 | }
10 |
11 | public static void initParent(File file) throws IOException {
12 | File parent = file.getParentFile();
13 | if (null != parent && !parent.exists() && !parent.mkdirs()) {
14 | throw new IOException("Can't create directory {" + parent.getAbsolutePath() + "}");
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/parser/SmaliClassNode.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.parser;
2 |
3 | public class SmaliClassNode extends SmaliBlockNode {
4 |
5 | public String getClassType() {
6 | return null;
7 | }
8 |
9 | @Override
10 | public SmaliClassNode createEmpty() {
11 | return new SmaliClassNode();
12 | }
13 |
14 | @Override
15 | public String getId() {
16 | return getClassName();
17 | }
18 |
19 | @Override
20 | public String getType() {
21 | return "class";
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/parser/SmaliParseResult.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.parser;
2 |
3 | import java.util.List;
4 |
5 | public class SmaliParseResult {
6 |
7 | private final T result;
8 | private final List errors;
9 |
10 | public SmaliParseResult(T result, List errors) {
11 | this.result = result;
12 | this.errors = errors;
13 | }
14 |
15 | public T getResult() {
16 | return result;
17 | }
18 |
19 | public List getErrors() {
20 | return errors;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/dex/SmaliException.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.dex;
2 |
3 | public class SmaliException extends Exception {
4 |
5 | private final String type;
6 | private final int lineIndex;
7 | private final int lineOffset;
8 |
9 | public SmaliException(String message) {
10 | this(message, null, 0, 0);
11 | }
12 |
13 | public SmaliException(String message, String type, int lineIndex, int lineOffset) {
14 | super(message);
15 | this.type = type;
16 | this.lineIndex = lineIndex;
17 | this.lineOffset = lineOffset;
18 | }
19 |
20 | public String getType() {
21 | return type;
22 | }
23 |
24 | public int getLineIndex() {
25 | return lineIndex;
26 | }
27 |
28 | public int getLineOffset() {
29 | return lineOffset;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
20 |
21 |
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/parser/SmaliFieldNode.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.parser;
2 |
3 | import java.util.List;
4 |
5 | public class SmaliFieldNode extends SmaliBlockNode {
6 |
7 | @Override
8 | public SmaliFieldNode createEmpty() {
9 | return new SmaliFieldNode();
10 | }
11 |
12 | @Override
13 | public String getId() {
14 | if (getChildCount() > 0) {
15 | List nodes = getChildren();
16 | SmaliToken word = null;
17 | for (int i = 0; i < nodes.size(); i++) {
18 | SmaliNode node = nodes.get(i);
19 | if ("token".equals(node.getType())) {
20 | SmaliToken token = (SmaliToken) node;
21 | if ("word".equals(token.getTokenType())) {
22 | word = token;
23 | } else if ("symbol".equals(token.getTokenType()) && ":".equals(token.getText())) {
24 | if (null != word) {
25 | return word.getText();
26 | }
27 | break;
28 | }
29 | }
30 | }
31 | }
32 |
33 | return null;
34 | }
35 |
36 | @Override
37 | public String getType() {
38 | return "field";
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/parser/SmaliEndNode.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.parser;
2 |
3 | public class SmaliEndNode extends SmaliNode {
4 |
5 | public String getBlockName() {
6 | if (getChildCount() > 0) {
7 | boolean start = false;
8 | for (SmaliNode node : getChildren()) {
9 | if ("token".equals(node.getType())) {
10 | SmaliToken token = (SmaliToken) node;
11 | if (start) {
12 | if ("word".equals(token.getTokenType())) {
13 | return token.getText();
14 | }
15 | } else {
16 | if ("block".equals(token.getTokenType()) && ".end".equals(token.getText())) {
17 | start = true;
18 | }
19 | }
20 | }
21 | }
22 | }
23 | return null;
24 | }
25 |
26 | @Override
27 | public SmaliEndNode copy() {
28 | SmaliEndNode node = new SmaliEndNode();
29 | if (getChildCount() > 0) {
30 | for (SmaliNode child : getChildren()) {
31 | node.getChildren().add(child.copy());
32 | }
33 | }
34 | return node;
35 | }
36 |
37 | @Override
38 | public String getType() {
39 | return "end";
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/plugin/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java-library'
3 | id 'maven-publish'
4 | id 'groovy'
5 | }
6 |
7 | dependencies {
8 | // gradle sdk
9 | implementation gradleApi()
10 | // groovy sdk
11 | implementation localGroovy()
12 |
13 | // baksmali
14 | implementation('org.smali:baksmali:2.5.2') {
15 | exclude group: 'com.google.guava', module: 'guava'
16 | }
17 | // smali
18 | implementation('org.smali:smali:2.5.2') {
19 | exclude group: 'com.google.guava', module: 'guava'
20 | }
21 |
22 | testImplementation 'junit:junit:4.13.2'
23 | }
24 |
25 | sourceCompatibility=JavaVersion.VERSION_1_8
26 | targetCompatibility=JavaVersion.VERSION_1_8
27 |
28 | javadoc {
29 | options {
30 | //如果你的项目里面有中文注释的话,必须将格式设置为UTF-8,不然会出现乱码
31 | encoding "UTF-8"
32 | charSet 'UTF-8'
33 | author true
34 | version true
35 | }
36 | }
37 |
38 | // Because the components are created only during the afterEvaluate phase, you must
39 | // configure your publications using the afterEvaluate() lifecycle method.
40 | afterEvaluate {
41 | publishing {
42 | publications {
43 | release(MavenPublication) {
44 | from components.java
45 | groupId = 'com.mosect'
46 | artifactId = 'smali-plugin'
47 | version = '1.2.0-b1'
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/util/RegexMatcher.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.util;
2 |
3 | import java.util.Objects;
4 | import java.util.regex.Matcher;
5 | import java.util.regex.Pattern;
6 |
7 | public class RegexMatcher {
8 |
9 | private final String regex;
10 | private Pattern pattern = null;
11 | private boolean complied;
12 |
13 | public RegexMatcher(String regex) {
14 | this.regex = regex;
15 | }
16 |
17 | public boolean matches(String text) {
18 | if (!complied) {
19 | try {
20 | pattern = Pattern.compile(regex);
21 | } catch (Exception ignored) {
22 | }
23 | complied = true;
24 | }
25 | if (null != pattern) {
26 | Matcher matcher = pattern.matcher(text);
27 | return matcher.matches();
28 | }
29 | return false;
30 | }
31 |
32 | @Override
33 | public boolean equals(Object o) {
34 | if (this == o) return true;
35 | if (o == null || getClass() != o.getClass()) return false;
36 | RegexMatcher that = (RegexMatcher) o;
37 | return regex.equals(that.regex);
38 | }
39 |
40 | @Override
41 | public int hashCode() {
42 | return Objects.hash(regex);
43 | }
44 |
45 | @Override
46 | public String toString() {
47 | return "RegexMatcher{" +
48 | "regex='" + regex + '\'' +
49 | '}';
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/parser/SmaliParseError.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.parser;
2 |
3 | public class SmaliParseError {
4 |
5 | private final String type;
6 | private final String message;
7 | private final int charIndex;
8 | private int lineIndex;
9 | private int lineOffset;
10 |
11 | public SmaliParseError(String type, String message, int charIndex) {
12 | this.type = type;
13 | this.message = message;
14 | this.charIndex = charIndex;
15 | }
16 |
17 | public String getType() {
18 | return type;
19 | }
20 |
21 | public String getMessage() {
22 | return message;
23 | }
24 |
25 | public int getCharIndex() {
26 | return charIndex;
27 | }
28 |
29 | public int getLineIndex() {
30 | return lineIndex;
31 | }
32 |
33 | public void setLineIndex(int lineIndex) {
34 | this.lineIndex = lineIndex;
35 | }
36 |
37 | public void setLineOffset(int lineOffset) {
38 | this.lineOffset = lineOffset;
39 | }
40 |
41 | public int getLineOffset() {
42 | return lineOffset;
43 | }
44 |
45 | @Override
46 | public String toString() {
47 | return "SmaliParseError{" +
48 | "type='" + type + '\'' +
49 | ", message='" + message + '\'' +
50 | ", charIndex=" + charIndex +
51 | ", lineIndex=" + lineIndex +
52 | ", lineOffset=" + lineOffset +
53 | '}';
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/dex/MethodNameMatcher.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.dex;
2 |
3 | import com.mosect.smali.plugin.parser.SmaliNode;
4 | import com.mosect.smali.plugin.parser.SmaliNodeMatcher;
5 | import com.mosect.smali.plugin.parser.SmaliToken;
6 |
7 | public class MethodNameMatcher implements SmaliNodeMatcher {
8 |
9 | private int state = 0;
10 | private int nameNodeIndex = -1;
11 |
12 | @Override
13 | public void reset() {
14 | state = 0;
15 | nameNodeIndex = -1;
16 | }
17 |
18 | public int getNameNodeIndex() {
19 | return nameNodeIndex;
20 | }
21 |
22 | @Override
23 | public int match(SmaliNode parent, SmaliNode node, int index) {
24 | switch (state) {
25 | case 0:
26 | if ("token".equals(node.getType())) {
27 | SmaliToken token = (SmaliToken) node;
28 | if ("word".equals(token.getTokenType())) {
29 | state = 1;
30 | nameNodeIndex = index;
31 | }
32 | }
33 | if (state != 1) return 0;
34 | return 1;
35 | case 1:
36 | if ("token".equals(node.getType())) {
37 | SmaliToken token = (SmaliToken) node;
38 | if ("symbol".equals(token.getTokenType()) && "(".equals(token.getText())) {
39 | state = 2;
40 | }
41 | }
42 | if (state != 2) return 0;
43 | return 2;
44 | }
45 | return 0;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/dex/DexDecoder.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.dex;
2 |
3 | import org.jf.baksmali.Baksmali;
4 | import org.jf.baksmali.BaksmaliOptions;
5 | import org.jf.dexlib2.DexFileFactory;
6 | import org.jf.dexlib2.Opcodes;
7 | import org.jf.dexlib2.iface.DexFile;
8 |
9 | import java.io.File;
10 | import java.io.IOException;
11 | import java.util.ArrayList;
12 | import java.util.List;
13 |
14 | public class DexDecoder {
15 |
16 | private final List dexFiles = new ArrayList<>();
17 | private int apiLevel = 15;
18 |
19 | public void setApiLevel(int apiLevel) {
20 | this.apiLevel = apiLevel;
21 | }
22 |
23 | public void addDexFile(File dexFile) {
24 | dexFiles.add(dexFile);
25 | }
26 |
27 | public List decode(File outDir) throws IOException {
28 | List result = new ArrayList<>();
29 |
30 | Opcodes opcodes = Opcodes.forApi(apiLevel);
31 | int classesIndex = 1;
32 | for (File file : dexFiles) {
33 | File dir;
34 | if (classesIndex == 1) {
35 | dir = new File(outDir, "classes");
36 | } else {
37 | dir = new File(outDir, "classes" + classesIndex);
38 | }
39 | DexFile dexFile = DexFileFactory.loadDexFile(file, opcodes);
40 | BaksmaliOptions options = new BaksmaliOptions();
41 | options.apiLevel = apiLevel;
42 | boolean ok = Baksmali.disassembleDexFile(dexFile, dir, 10, options);
43 | if (!ok) {
44 | throw new IOException("BakSmaliFailed: " + file.getAbsolutePath());
45 | }
46 | result.add(dir);
47 | ++classesIndex;
48 | }
49 | return result;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/dex/SimpleSmaliParser.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.dex;
2 |
3 | import com.mosect.smali.plugin.parser.SmaliBlockNode;
4 | import com.mosect.smali.plugin.parser.SmaliClassNode;
5 | import com.mosect.smali.plugin.parser.SmaliParseError;
6 | import com.mosect.smali.plugin.parser.SmaliParseResult;
7 | import com.mosect.smali.plugin.parser.SmaliParser;
8 | import com.mosect.smali.plugin.util.TextUtils;
9 |
10 | import java.io.File;
11 | import java.io.IOException;
12 |
13 | public class SimpleSmaliParser extends SmaliParser {
14 |
15 | public SmaliBlockNode parse(File smaliFile) throws IOException, SmaliException {
16 | SmaliParseResult result = parseFileWithUtf8(smaliFile);
17 | if (!result.getErrors().isEmpty()) {
18 | SmaliParseError error = result.getErrors().get(0);
19 | throw createException(smaliFile, error);
20 | }
21 | SmaliClassNode classNode = result.getResult().findNode("class");
22 | if (null == classNode) {
23 | throw createException(smaliFile, new SmaliParseError("CLASS:MISSING_CLASS_NODE", "Missing class node", 0));
24 | }
25 | if (TextUtils.isEmpty(classNode.getClassName())) {
26 | throw createException(smaliFile, new SmaliParseError("CLASS:INVALID_CLASS_NODE", "Invalid class node", 0));
27 | }
28 | return result.getResult();
29 | }
30 |
31 | private SmaliException createException(File file, SmaliParseError error) {
32 | String errorMsg = String.format(
33 | "ErrorSmali{%s}[%s:%s]>>> %s",
34 | file.getAbsolutePath(),
35 | error.getLineIndex() + 1,
36 | error.getLineOffset() + 1,
37 | error.getMessage()
38 | );
39 | return new SmaliException(errorMsg, error.getType(), error.getLineIndex(), error.getLineOffset());
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.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 |
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/dex/CopyValueMatcher.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.dex;
2 |
3 | import com.mosect.smali.plugin.parser.SmaliNode;
4 | import com.mosect.smali.plugin.parser.SmaliNodeMatcher;
5 | import com.mosect.smali.plugin.parser.SmaliToken;
6 |
7 | public class CopyValueMatcher implements SmaliNodeMatcher {
8 |
9 | private int state = 0;
10 | private String value;
11 |
12 | @Override
13 | public void reset() {
14 | state = 0;
15 | value = null;
16 | }
17 |
18 | public String getValue() {
19 | return value;
20 | }
21 |
22 | @Override
23 | public int match(SmaliNode parent, SmaliNode node, int index) {
24 | switch (state) {
25 | case 0:
26 | if ("token".equals(node.getType())) {
27 | SmaliToken token = (SmaliToken) node;
28 | if ("word".equals(token.getTokenType()) && "value".equals(token.getText())) {
29 | state = 1;
30 | }
31 | }
32 | if (state != 1) return 0;
33 | return 1;
34 | case 1:
35 | if ("token".equals(node.getType())) {
36 | SmaliToken token = (SmaliToken) node;
37 | if ("symbol".equals(token.getTokenType()) && "=".equals(token.getText())) {
38 | state = 2;
39 | }
40 | }
41 | if (state != 2) return 0;
42 | return 1;
43 | case 2:
44 | if ("token".equals(node.getType())) {
45 | SmaliToken token = (SmaliToken) node;
46 | if ("string".equals(token.getTokenType())) {
47 | state = 3;
48 | String text = token.getText();
49 | value = text.substring(1, text.length() - 1);
50 | }
51 | }
52 | if (state != 3) return 0;
53 | return 2;
54 | }
55 | return 0;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/plugin/src/main/groovy/com/mosect/smali/plugin/SmaliExtension.groovy:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin
2 |
3 | class SmaliExtension {
4 |
5 | private final List dirs = []
6 | private final List operationFiles = []
7 | private final List positionFiles = []
8 | private Integer apiLevel = null
9 |
10 | List getDirs() {
11 | return dirs
12 | }
13 |
14 | void setDirs(List dirs) {
15 | this.dirs.clear()
16 | if (dirs) {
17 | this.dirs.addAll(dirs)
18 | }
19 | }
20 |
21 | void addDirs(File... dirs) {
22 | if (dirs) {
23 | this.dirs.addAll(dirs)
24 | }
25 | }
26 |
27 | void addDir(File dir) {
28 | this.dirs.add(dir)
29 | }
30 |
31 | List getOperationFiles() {
32 | return this.operationFiles
33 | }
34 |
35 | void setOperationFiles(List files) {
36 | this.operationFiles.clear()
37 | if (files) {
38 | this.operationFiles.addAll(files)
39 | }
40 | }
41 |
42 | void addOperationFiles(File... files) {
43 | if (files) {
44 | this.operationFiles.addAll(files)
45 | }
46 | }
47 |
48 | void addOperationFile(File file) {
49 | if (null != file) {
50 | this.operationFiles.add(file)
51 | }
52 | }
53 |
54 | List getPositionFiles() {
55 | return this.positionFiles
56 | }
57 |
58 | void setPositionFiles(List files) {
59 | this.positionFiles.clear()
60 | if (files) {
61 | this.positionFiles.addAll(files)
62 | }
63 | }
64 |
65 | void addPositionFiles(File... files) {
66 | if (files) {
67 | this.positionFiles.addAll(files)
68 | }
69 | }
70 |
71 | void addPositionFile(File file) {
72 | if (null != file) {
73 | this.positionFiles.add(file)
74 | }
75 | }
76 |
77 | void setApiLevel(Integer apiLevel) {
78 | this.apiLevel = apiLevel
79 | }
80 |
81 | Integer getApiLevel() {
82 | return this.apiLevel
83 | }
84 | }
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/util/TextUtils.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.util;
2 |
3 | public final class TextUtils {
4 |
5 | private TextUtils() {
6 | }
7 |
8 | public static boolean isEmpty(CharSequence cs) {
9 | return null == cs || cs.length() <= 0;
10 | }
11 |
12 | public static String convertToRegex(String text) {
13 | StringBuilder builder = new StringBuilder((int) (text.length() * 1.618f));
14 | builder.append('^');
15 | int offset = 0;
16 | while (offset < text.length()) {
17 | if (match(text, offset, "**")) {
18 | builder.append("\\S*");
19 | offset += 2;
20 | } else {
21 | char ch = text.charAt(offset);
22 | switch (ch) {
23 | case '*':
24 | builder.append("[^.]*");
25 | break;
26 | case '^':
27 | case '.':
28 | case '[':
29 | case ']':
30 | case '\\':
31 | case '$':
32 | case '(':
33 | case ')':
34 | case '{':
35 | case '}':
36 | case '?':
37 | case '+':
38 | case '|':
39 | builder.append('\\').append(ch);
40 | break;
41 | default:
42 | builder.append(ch);
43 | break;
44 | }
45 | ++offset;
46 | }
47 | }
48 | builder.append('$');
49 | return builder.toString();
50 | }
51 |
52 | private static boolean match(String target, int offset, String str) {
53 | if (target.length() - offset >= str.length()) {
54 | for (int i = 0; i < str.length(); i++) {
55 | if (target.charAt(offset + i) != str.charAt(i)) {
56 | return false;
57 | }
58 | }
59 | return true;
60 | }
61 | return false;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/parser/SmaliNode.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.parser;
2 |
3 | import java.io.IOException;
4 | import java.util.ArrayList;
5 | import java.util.List;
6 |
7 | public abstract class SmaliNode {
8 |
9 | private List children;
10 |
11 | public int getChildCount() {
12 | if (null != children) return children.size();
13 | return 0;
14 | }
15 |
16 | public List getChildren() {
17 | if (null == children) children = new ArrayList<>();
18 | return children;
19 | }
20 |
21 | public void setChildren(List children) {
22 | this.children = children;
23 | }
24 |
25 | public void append(Appendable appendable) throws IOException {
26 | if (getChildCount() > 0) {
27 | for (SmaliNode child : getChildren()) {
28 | child.append(appendable);
29 | }
30 | }
31 | }
32 |
33 | public int length() {
34 | int len = 0;
35 | if (getChildCount() > 0) {
36 | for (SmaliNode child : getChildren()) {
37 | len += child.length();
38 | }
39 | }
40 | return len;
41 | }
42 |
43 | public abstract SmaliNode copy();
44 |
45 | public abstract String getType();
46 |
47 | public boolean match(SmaliNodeMatcher matcher) {
48 | for (int i = 0; i < getChildCount(); i++) {
49 | matcher.reset();
50 | SmaliNode child = getChildren().get(i);
51 | if (isIgnoreNode(child)) continue;
52 | for (int j = i; j < getChildCount(); j++) {
53 | SmaliNode cur = getChildren().get(j);
54 | if (isIgnoreNode(cur)) continue;
55 | int matchValue = matcher.match(this, cur, j);
56 | if (matchValue == 0) break;
57 | if (matchValue == 2) {
58 | // 匹配成功
59 | return true;
60 | }
61 | }
62 | }
63 | return false;
64 | }
65 |
66 | protected boolean isIgnoreNode(SmaliNode node) {
67 | if ("token".equals(node.getType())) {
68 | SmaliToken token = (SmaliToken) node;
69 | return "comment".equals(token.getTokenType()) || "whitespace".equals(token.getTokenType());
70 | }
71 | return false;
72 | }
73 |
74 | @Override
75 | public String toString() {
76 | return "SmaliNode{type=" + getType() + "}";
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/parser/SmaliToken.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.parser;
2 |
3 | import java.io.IOException;
4 | import java.util.Collections;
5 | import java.util.List;
6 |
7 | public class SmaliToken extends SmaliNode {
8 |
9 | public static SmaliToken lineCr() {
10 | return createLineToken("\r");
11 | }
12 |
13 | public static SmaliToken lineCrlf() {
14 | return createLineToken("\r\n");
15 | }
16 |
17 | public static SmaliToken lineLf() {
18 | return createLineToken("\n");
19 | }
20 |
21 | public static SmaliToken line() {
22 | return createLineToken(System.lineSeparator());
23 | }
24 |
25 | private static SmaliToken createLineToken(String str) {
26 | switch (str) {
27 | case "\r":
28 | return new SmaliToken(str, "line.cr");
29 | case "\n":
30 | return new SmaliToken(str, "line.lf");
31 | case "\r\n":
32 | return new SmaliToken(str, "line.crlf");
33 | default:
34 | throw new IllegalArgumentException("Unsupported line text");
35 | }
36 | }
37 |
38 | private final String tokenType;
39 | private final String text;
40 |
41 | public SmaliToken(String text, String tokenType) {
42 | this.text = text;
43 | this.tokenType = tokenType;
44 | }
45 |
46 | public String getText() {
47 | return text;
48 | }
49 |
50 | public String getTokenType() {
51 | return tokenType;
52 | }
53 |
54 | @Override
55 | public int getChildCount() {
56 | return 0;
57 | }
58 |
59 | @Override
60 | public int length() {
61 | return text.length();
62 | }
63 |
64 | @Override
65 | public SmaliNode copy() {
66 | return new SmaliToken(text, tokenType);
67 | }
68 |
69 | @Override
70 | public List getChildren() {
71 | return Collections.emptyList();
72 | }
73 |
74 | @Override
75 | public void setChildren(List children) {
76 | throw new UnsupportedOperationException();
77 | }
78 |
79 | @Override
80 | public void append(Appendable appendable) throws IOException {
81 | appendable.append(text);
82 | }
83 |
84 | @Override
85 | public String getType() {
86 | return "token";
87 | }
88 |
89 | @Override
90 | public String toString() {
91 | return "SmaliToken{" +
92 | "tokenType='" + tokenType + '\'' +
93 | ", text='" + text + '\'' +
94 | '}';
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/parser/SmaliMethodNode.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.parser;
2 |
3 | import java.io.IOException;
4 | import java.util.List;
5 |
6 | public class SmaliMethodNode extends SmaliBlockNode {
7 |
8 | @Override
9 | public SmaliMethodNode createEmpty() {
10 | return new SmaliMethodNode();
11 | }
12 |
13 | @Override
14 | public String getId() {
15 | if (getChildCount() > 0) {
16 | List nodes = getChildren();
17 | int end = -1;
18 | int start = -1;
19 | int wordIndex = -1;
20 | _for:
21 | for (int i = 0; i < nodes.size(); i++) {
22 | SmaliNode node = nodes.get(i);
23 | if ("token".equals(node.getType())) {
24 | SmaliToken token = (SmaliToken) node;
25 | if (start < 0) {
26 | if ("symbol".equals(token.getTokenType()) && "(".equals(token.getText())) {
27 | if (wordIndex < 0) return null;
28 | start = wordIndex;
29 | } else if ("word".equals(token.getTokenType())) {
30 | wordIndex = i;
31 | }
32 | } else {
33 | if ("symbol".equals(token.getTokenType()) && ")".equals(token.getText())) {
34 | end = i + 1;
35 | break;
36 | } else {
37 | switch (token.getTokenType()) {
38 | case "comment":
39 | case "whitespace":
40 | case "word":
41 | break;
42 | default:
43 | break _for;
44 | }
45 | }
46 | }
47 | }
48 | }
49 | if (end >= 0) {
50 | try {
51 | StringBuilder builder = new StringBuilder(128);
52 | for (int i = start; i < end; i++) {
53 | SmaliNode node = nodes.get(i);
54 | if ("token".equals(node.getType())) {
55 | SmaliToken token = (SmaliToken) node;
56 | if ("word".equals(token.getTokenType()) || "symbol".equals(token.getTokenType())) {
57 | token.append(builder);
58 | }
59 | }
60 | }
61 | return builder.toString();
62 | } catch (IOException ignored) {
63 | }
64 | }
65 | }
66 |
67 | return null;
68 | }
69 |
70 | @Override
71 | public String getType() {
72 | return "method";
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/dex/DexMaker.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.dex;
2 |
3 | import com.mosect.smali.plugin.parser.SmaliBlockNode;
4 | import com.mosect.smali.plugin.util.IOUtils;
5 | import com.mosect.smali.plugin.util.TextUtils;
6 |
7 | import org.jf.smali.Smali;
8 | import org.jf.smali.SmaliOptions;
9 |
10 | import java.io.File;
11 | import java.io.IOException;
12 | import java.util.ArrayList;
13 | import java.util.HashMap;
14 | import java.util.List;
15 | import java.util.Map;
16 | import java.util.Set;
17 |
18 | public class DexMaker {
19 |
20 | private final int index;
21 | private final String name;
22 | private final Map smaliFileMap = new HashMap<>();
23 | private final SimpleSmaliParser smaliParser = new SimpleSmaliParser();
24 | private int apiLevel = 15;
25 |
26 | public DexMaker(int index) {
27 | if (index < 1 || index > 99) {
28 | throw new IllegalArgumentException("Invalid dex index: " + index);
29 | }
30 | this.index = index;
31 | if (index == 1) {
32 | name = "classes";
33 | } else {
34 | name = "classes" + index;
35 | }
36 | }
37 |
38 | public int getIndex() {
39 | return index;
40 | }
41 |
42 | public String getName() {
43 | return name;
44 | }
45 |
46 | public void setApiLevel(int apiLevel) {
47 | this.apiLevel = apiLevel;
48 | }
49 |
50 | public void addSmaliFile(String className, File smaliFile) throws IOException, SmaliException {
51 | String safeClassName;
52 | if (TextUtils.isEmpty(className)) {
53 | SmaliBlockNode blockNode = smaliParser.parse(smaliFile);
54 | safeClassName = blockNode.getClassName();
55 | } else {
56 | safeClassName = className;
57 | }
58 | // System.out.printf("AddSmaliFile: [%s]{%s}%n", safeClassName, smaliFile.getAbsolutePath());
59 | smaliFileMap.put(safeClassName, smaliFile);
60 | }
61 |
62 | public File getSmaliFile(String className) {
63 | return smaliFileMap.get(className);
64 | }
65 |
66 | public File removeSmaliFile(String className) {
67 | return smaliFileMap.remove(className);
68 | }
69 |
70 | /**
71 | * Get all classes
72 | *
73 | * @return All classes, class name list
74 | */
75 | public Set> allClasses() {
76 | return smaliFileMap.entrySet();
77 | }
78 |
79 | public boolean makeDex(File outFile) throws IOException {
80 | IOUtils.initParent(outFile);
81 | SmaliOptions options = new SmaliOptions();
82 | options.apiLevel = apiLevel;
83 | options.jobs = 10;
84 | options.outputDexFile = outFile.getAbsolutePath();
85 | List files = new ArrayList<>();
86 | for (File file : smaliFileMap.values()) {
87 | files.add(file.getAbsolutePath());
88 | }
89 | return Smali.assemble(options, files);
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/dex/MemberOperation.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.dex;
2 |
3 | import com.mosect.smali.plugin.util.RegexMatcher;
4 | import com.mosect.smali.plugin.util.TextUtils;
5 |
6 | import java.util.HashMap;
7 | import java.util.HashSet;
8 | import java.util.Map;
9 | import java.util.Objects;
10 |
11 | public class MemberOperation {
12 |
13 | private final Map> data = new HashMap<>();
14 |
15 | public boolean addLine(String line) {
16 | String[] fs = line.split("\\s+");
17 | if (fs.length != 2) {
18 | return false;
19 | } else {
20 | String type;
21 | String regex;
22 | if (fs[1].matches("^[^()]+\\([^()]*\\)$")) {
23 | // 方法
24 | type = "method";
25 | regex = TextUtils.convertToRegex(fs[1]);
26 | } else if (fs[1].matches("^[^()]+$")) {
27 | // 字段
28 | type = "field";
29 | regex = TextUtils.convertToRegex(fs[1]);
30 | } else {
31 | return false;
32 | }
33 |
34 | String action;
35 | switch (fs[0]) {
36 | case "delete":
37 | case "d":
38 | action = "d";
39 | break;
40 | case "ignore":
41 | case "i":
42 | action = "i";
43 | break;
44 | case "replace":
45 | case "r":
46 | action = "r";
47 | break;
48 | case "original":
49 | case "o":
50 | action = "o";
51 | break;
52 | default:
53 | return false;
54 | }
55 |
56 | Key key = new Key(type, action);
57 | HashSet regexList = data.computeIfAbsent(key, k -> new HashSet<>());
58 | regexList.add(new RegexMatcher(regex));
59 | return true;
60 | }
61 | }
62 |
63 | public boolean matchDelete(String type, String id) {
64 | return match(type, "d", id);
65 | }
66 |
67 | public boolean matchIgnore(String type, String id) {
68 | return match(type, "i", id);
69 | }
70 |
71 | public boolean matchReplace(String type, String id) {
72 | return match(type, "r", id);
73 | }
74 |
75 | public boolean matchOriginal(String type, String id) {
76 | return match(type, "o", id);
77 | }
78 |
79 | private boolean match(String type, String action, String id) {
80 | Key key = new Key(type, action);
81 | HashSet regexList = data.get(key);
82 | if (null != regexList) {
83 | for (RegexMatcher regex : regexList) {
84 | if (regex.matches(id)) {
85 | return true;
86 | }
87 | }
88 | }
89 | return false;
90 | }
91 |
92 | private static class Key {
93 | private final String type;
94 | private final String action;
95 |
96 | public Key(String type, String action) {
97 | this.type = type;
98 | this.action = action;
99 | }
100 |
101 | @Override
102 | public boolean equals(Object o) {
103 | if (this == o) return true;
104 | if (o == null || getClass() != o.getClass()) return false;
105 | Key key = (Key) o;
106 | return type.equals(key.type) && action.equals(key.action);
107 | }
108 |
109 | @Override
110 | public int hashCode() {
111 | return Objects.hash(type, action);
112 | }
113 |
114 | @Override
115 | public String toString() {
116 | return "Key{" +
117 | "type='" + type + '\'' +
118 | ", action='" + action + '\'' +
119 | '}';
120 | }
121 | }
122 |
123 | }
124 |
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/dex/ClassesSource.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.dex;
2 |
3 | import java.io.File;
4 | import java.io.FileFilter;
5 | import java.util.ArrayList;
6 | import java.util.HashSet;
7 | import java.util.List;
8 | import java.util.Objects;
9 |
10 | public class ClassesSource {
11 |
12 | private final HashSet dirs = new HashSet<>();
13 |
14 | public void addDir(File dir) {
15 | dirs.add(dir);
16 | }
17 |
18 | /**
19 | * 获取smali文件
20 | *
21 | * @param dir 目录
22 | * @param className 类名
23 | * @return smali文件
24 | */
25 | public static File getSmaliFile(File dir, String className) {
26 | String path = getClassPath(className);
27 | return new File(dir, path);
28 | }
29 |
30 | /**
31 | * 查找smali文件
32 | *
33 | * @param className 类名
34 | * @return smali文件,找不到返回null
35 | */
36 | public File findSmaliFile(String className) {
37 | String path = getClassPath(className);
38 | for (File dir : dirs) {
39 | File file = new File(dir, path);
40 | if (file.exists() && file.isFile()) {
41 | return file;
42 | }
43 | }
44 | return null;
45 | }
46 |
47 | /**
48 | * 列出所有smali文件
49 | *
50 | * @return 所有smali文件
51 | */
52 | public List listAll() {
53 | List list = new ArrayList<>(128);
54 | for (File dir : dirs) {
55 | listByDir(dir, list, "");
56 | }
57 | return list;
58 | }
59 |
60 | private static String getClassPath(String className) {
61 | return className.replace('.', '/') + ".smali";
62 | }
63 |
64 | private void listByDir(File dir, List out, String namePrefix) {
65 | File[] files = dir.listFiles(new FileFilter() {
66 | @Override
67 | public boolean accept(File file) {
68 | return file.isFile() && file.getName().endsWith(".smali");
69 | }
70 | });
71 | if (null != files) {
72 | for (File file : files) {
73 | String name = file.getName();
74 | String className = namePrefix + name.substring(0, name.length() - ".smali".length());
75 | out.add(new SmaliFileInfo(className, file));
76 | }
77 | }
78 | File[] dirs = dir.listFiles(new FileFilter() {
79 | @Override
80 | public boolean accept(File file) {
81 | String name = file.getName();
82 | if (file.isDirectory()) {
83 | return !".".equals(name) && !"..".equals(name);
84 | }
85 | return false;
86 | }
87 | });
88 | if (null != dirs) {
89 | for (File childDir : dirs) {
90 | String nextNamePrefix = namePrefix + childDir.getName() + ".";
91 | listByDir(childDir, out, nextNamePrefix);
92 | }
93 | }
94 | }
95 |
96 | public static class SmaliFileInfo {
97 |
98 | private final String className;
99 | private final File file;
100 |
101 | public SmaliFileInfo(String className, File file) {
102 | this.className = className;
103 | this.file = file;
104 | }
105 |
106 | public String getClassName() {
107 | return className;
108 | }
109 |
110 | public File getFile() {
111 | return file;
112 | }
113 |
114 | @Override
115 | public boolean equals(Object o) {
116 | if (this == o) return true;
117 | if (o == null || getClass() != o.getClass()) return false;
118 | SmaliFileInfo that = (SmaliFileInfo) o;
119 | return className.equals(that.className);
120 | }
121 |
122 | @Override
123 | public int hashCode() {
124 | return Objects.hash(className);
125 | }
126 |
127 | @Override
128 | public String toString() {
129 | return "SmaliFileInfo{" +
130 | "className='" + className + '\'' +
131 | ", file=" + file +
132 | '}';
133 | }
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/parser/SmaliBlockNode.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.parser;
2 |
3 | import java.util.Objects;
4 |
5 | /**
6 | * 区块节点
7 | */
8 | public class SmaliBlockNode extends SmaliNode {
9 |
10 | public String getBlockName() {
11 | if (getChildCount() > 0) {
12 | for (SmaliNode node : getChildren()) {
13 | if ("token".equals(node.getType())) {
14 | SmaliToken token = (SmaliToken) node;
15 | if ("block".equals(token.getTokenType())) {
16 | return token.getText();
17 | }
18 | }
19 | }
20 | }
21 | return null;
22 | }
23 |
24 | @SuppressWarnings("unchecked")
25 | public T findNode(String nodeType) {
26 | if (getChildCount() > 0) {
27 | for (SmaliNode node : getChildren()) {
28 | if (Objects.equals(node.getType(), nodeType)) {
29 | return (T) node;
30 | }
31 | }
32 | }
33 | return null;
34 | }
35 |
36 | public String getClassName() {
37 | if (getChildCount() > 0) {
38 | int mode = 0;
39 | StringBuilder builder = new StringBuilder(64);
40 | _for:
41 | for (SmaliNode node : getChildren()) {
42 | if ("token".equals(node.getType())) {
43 | SmaliToken token = (SmaliToken) node;
44 | switch (mode) {
45 | case 0:
46 | if ("block".equals(token.getTokenType())) {
47 | mode = 1;
48 | }
49 | break;
50 | case 1:
51 | if ("word".equals(token.getTokenType())) {
52 | String text = token.getText();
53 | boolean end = text.endsWith(";");
54 | if (text.startsWith("L")) {
55 | mode = 2;
56 | if (end) {
57 | builder.append(text, 1, text.length() - 1);
58 | } else {
59 | builder.append(text, 1, text.length());
60 | }
61 | }
62 | }
63 | break;
64 | case 2:
65 | if (!"whitespace".equals(token.getTokenType()) && !"comment".equals(token.getTokenType())) {
66 | if ("word".equals(token.getTokenType())) {
67 | String text = token.getText();
68 | boolean end = text.endsWith(";");
69 | if (end) {
70 | builder.append(text, 0, text.length() - 1);
71 | break _for;
72 | } else {
73 | builder.append(text);
74 | }
75 | } else {
76 | break _for;
77 | }
78 | }
79 | break;
80 | }
81 | }
82 | }
83 | for (int i = 0; i < builder.length(); i++) {
84 | char ch = builder.charAt(i);
85 | if (ch == '/') {
86 | builder.setCharAt(i, '.');
87 | }
88 | }
89 | return builder.toString();
90 | }
91 | return null;
92 | }
93 |
94 | public String getId() {
95 | return null;
96 | }
97 |
98 | @Override
99 | public SmaliBlockNode copy() {
100 | SmaliBlockNode node = createEmpty();
101 | if (getChildCount() > 0) {
102 | for (SmaliNode child : getChildren()) {
103 | node.getChildren().add(child.copy());
104 | }
105 | }
106 | return node;
107 | }
108 |
109 | public SmaliBlockNode createEmpty() {
110 | return new SmaliBlockNode();
111 | }
112 |
113 | @Override
114 | public String getType() {
115 | return "block";
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/dex/ClassPosition.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.dex;
2 |
3 | import com.mosect.smali.plugin.util.RegexMatcher;
4 | import com.mosect.smali.plugin.util.TextUtils;
5 |
6 | import java.io.BufferedReader;
7 | import java.io.File;
8 | import java.io.FileInputStream;
9 | import java.io.IOException;
10 | import java.io.InputStreamReader;
11 | import java.nio.charset.StandardCharsets;
12 | import java.util.HashSet;
13 | import java.util.Objects;
14 |
15 | /**
16 | * Class's dex index handle
17 | */
18 | public class ClassPosition {
19 |
20 | public final static int INDEX_NEW = -1;
21 | public final static int INDEX_END = -2;
22 |
23 | private final HashSet data = new HashSet<>();
24 |
25 | public void load(File file) throws IOException {
26 | try (FileInputStream fis = new FileInputStream(file);
27 | InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8);
28 | BufferedReader br = new BufferedReader(isr)) {
29 | String line;
30 | while ((line = br.readLine()) != null) {
31 | if (line.startsWith("#")) {
32 | continue;
33 | }
34 |
35 | String[] fs = line.split("\\s+");
36 | if (fs.length == 2) {
37 | int targetIndex;
38 | if ("new".equals(fs[0])) {
39 | targetIndex = -1;
40 | } else if ("end".equals(fs[0])) {
41 | targetIndex = -2;
42 | } else if ("main".equals(fs[0])) {
43 | targetIndex = 1;
44 | } else if (fs[0].matches("^[0-9]+$")) {
45 | targetIndex = Integer.parseInt(fs[0]);
46 | if (targetIndex <= 0 || targetIndex > 99) {
47 | invalidLine(line);
48 | continue;
49 | }
50 | } else {
51 | invalidLine(line);
52 | continue;
53 | }
54 |
55 | int classIndex;
56 | String classPath;
57 | if (fs[1].matches("^[0-9]+:\\S+$")) {
58 | int si = fs[1].indexOf(":");
59 | String indexStr = fs[1].substring(0, si);
60 | classPath = fs[1].substring(si + 1);
61 | classIndex = Integer.parseInt(indexStr);
62 | if (TextUtils.isEmpty(classPath)) {
63 | invalidLine(line);
64 | continue;
65 | }
66 | } else {
67 | classIndex = 0;
68 | classPath = fs[1];
69 | }
70 |
71 | String regex = TextUtils.convertToRegex(classPath);
72 | PositionItem positionItem = new PositionItem(regex, classIndex, targetIndex);
73 | data.add(positionItem);
74 | } else {
75 | invalidLine(line);
76 | }
77 | }
78 | }
79 | }
80 |
81 | private void invalidLine(String line) {
82 | System.err.printf("ClassPosition:invalidLine {%s}%n", line);
83 | }
84 |
85 | public PositionItem find(int dexIndex, String className) {
86 | for (PositionItem pi : data) {
87 | if ((pi.getFromIndex() == 0 || pi.getFromIndex() == dexIndex) && pi.matchClassName(className)) {
88 | return pi;
89 | }
90 | }
91 | return null;
92 | }
93 |
94 | public static class PositionItem {
95 |
96 | private final String regex;
97 | private final int fromIndex;
98 | private final int toIndex;
99 | private RegexMatcher regexMatcher;
100 |
101 | private PositionItem(String regex, int fromIndex, int toIndex) {
102 | this.regex = regex;
103 | this.fromIndex = fromIndex;
104 | this.toIndex = toIndex;
105 | }
106 |
107 | public int getFromIndex() {
108 | return fromIndex;
109 | }
110 |
111 | public int getToIndex() {
112 | return toIndex;
113 | }
114 |
115 | @Override
116 | public boolean equals(Object o) {
117 | if (this == o) return true;
118 | if (o == null || getClass() != o.getClass()) return false;
119 | PositionItem that = (PositionItem) o;
120 | return regex.equals(that.regex);
121 | }
122 |
123 | @Override
124 | public int hashCode() {
125 | return Objects.hash(regex);
126 | }
127 |
128 | @Override
129 | public String toString() {
130 | return "OperationItem{" +
131 | "regex='" + regex + '\'' +
132 | ", fromIndex=" + fromIndex +
133 | ", toIndex=" + toIndex +
134 | '}';
135 | }
136 |
137 | private boolean matchClassName(String className) {
138 | if (null == regexMatcher) {
139 | regexMatcher = new RegexMatcher(regex);
140 | }
141 | return regexMatcher.matches(className);
142 | }
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/dex/DexHandler.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.dex;
2 |
3 | import java.io.File;
4 | import java.io.IOException;
5 | import java.util.ArrayList;
6 | import java.util.HashMap;
7 | import java.util.HashSet;
8 | import java.util.List;
9 | import java.util.Map;
10 | import java.util.Set;
11 |
12 | public class DexHandler {
13 |
14 | private final Map> originalSourceDirMap = new HashMap<>();
15 | private final HashSet javaDexFiles = new HashSet<>();
16 | private File tempDir;
17 | private int apiLevel = 15;
18 | private final List operationFiles = new ArrayList<>();
19 | private final List positionFiles = new ArrayList<>();
20 |
21 | public void setApiLevel(int apiLevel) {
22 | this.apiLevel = apiLevel;
23 | }
24 |
25 | public void addOriginalSourceDir(int dexIndex, File dir) {
26 | HashSet files = originalSourceDirMap.computeIfAbsent(dexIndex, k -> new HashSet<>());
27 | files.add(dir);
28 | }
29 |
30 | public Set allOriginalSourceClasses() {
31 | return originalSourceDirMap.keySet();
32 | }
33 |
34 | public void addJavaDexFile(File dexFile) {
35 | javaDexFiles.add(dexFile);
36 | }
37 |
38 | public void setTempDir(File tempDir) {
39 | this.tempDir = tempDir;
40 | }
41 |
42 | public void addOperationFile(File file) {
43 | operationFiles.add(file);
44 | }
45 |
46 | public void addPositionFile(File file) {
47 | positionFiles.add(file);
48 | }
49 |
50 | /**
51 | * 执行dex处理任务
52 | *
53 | * @return 最终dex输出的目录
54 | * @throws IOException 读写异常
55 | * @throws SmaliException smali异常
56 | */
57 | public File run() throws IOException, SmaliException {
58 | if (null == tempDir) {
59 | throw new IllegalArgumentException("tempDir not set");
60 | }
61 |
62 | File javaTempDir = new File(tempDir, "smali");
63 | javaTempDir.mkdirs();
64 |
65 | List javaClassesDirs = null;
66 | if (javaDexFiles.size() > 0) {
67 | System.out.println("DexHandler:dexDecode");
68 | DexDecoder dexDecoder = new DexDecoder();
69 | dexDecoder.setApiLevel(apiLevel);
70 | for (File file : javaDexFiles) {
71 | dexDecoder.addDexFile(file);
72 | }
73 | javaClassesDirs = dexDecoder.decode(javaTempDir);
74 | }
75 |
76 | // 创建java的classesSource
77 | ClassesSource javaClassesSource = new ClassesSource();
78 | if (null != javaClassesDirs) {
79 | for (File dir : javaClassesDirs) {
80 | javaClassesSource.addDir(dir);
81 | }
82 | }
83 |
84 | // 构建dexMaker
85 | List dexMakerList = new ArrayList<>();
86 | for (Map.Entry> entry : originalSourceDirMap.entrySet()) {
87 | int dexIndex = entry.getKey();
88 | DexMaker dexMaker = new DexMaker(dexIndex);
89 | ClassesSource classesSource = new ClassesSource();
90 | for (File dir : entry.getValue()) {
91 | classesSource.addDir(dir);
92 | }
93 | List fileInfoList = classesSource.listAll();
94 | for (ClassesSource.SmaliFileInfo fileInfo : fileInfoList) {
95 | dexMaker.addSmaliFile(fileInfo.getClassName(), fileInfo.getFile());
96 | }
97 | dexMaker.setApiLevel(apiLevel);
98 | System.out.println("DexHandler:dexMaker " + dexMaker.getName());
99 | dexMakerList.add(dexMaker);
100 | }
101 | if (null == originalSourceDirMap.get(1)) {
102 | // 不存在主dex构建器
103 | DexMaker dexMaker = new DexMaker(1);
104 | dexMaker.setApiLevel(apiLevel);
105 | dexMakerList.add(0, dexMaker);
106 | }
107 |
108 | // 合并smali
109 | System.out.println("DexHandler:mergeSmali");
110 | File mergedTempDir = new File(tempDir, "merged");
111 | SmaliMerger merger = new SmaliMerger();
112 | merger.setJavaSource(javaClassesSource);
113 | merger.setTempDir(mergedTempDir);
114 | for (DexMaker dexMaker : dexMakerList) {
115 | merger.addDexMaker(dexMaker);
116 | }
117 | for (File file : operationFiles) {
118 | if (file.isFile()) {
119 | System.out.printf("DexHandler:addOperationFile {%s}%n", file.getAbsolutePath());
120 | merger.addOperationFile(file);
121 | }
122 | }
123 | for (File file : positionFiles) {
124 | if (file.isFile()) {
125 | System.out.printf("DexHandler:addPositionFile {%s}%n", file.getAbsolutePath());
126 | merger.addPositionFile(file);
127 | }
128 | }
129 | dexMakerList = merger.merge();
130 |
131 | // 构建dex
132 | File classesDir = new File(tempDir, "classes");
133 | for (DexMaker dexMaker : dexMakerList) {
134 | File file = new File(classesDir, dexMaker.getName() + ".dex");
135 | System.out.printf("DexHandler:makeDex[%s] %s%n", dexMaker.getName(), file.getAbsolutePath());
136 | dexMaker.makeDex(file);
137 | }
138 | return classesDir;
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/dex/ClassOperation.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.dex;
2 |
3 | import com.mosect.smali.plugin.util.RegexMatcher;
4 | import com.mosect.smali.plugin.util.TextUtils;
5 |
6 | import java.io.BufferedReader;
7 | import java.io.File;
8 | import java.io.FileInputStream;
9 | import java.io.IOException;
10 | import java.io.InputStreamReader;
11 | import java.nio.charset.StandardCharsets;
12 | import java.util.HashMap;
13 | import java.util.HashSet;
14 | import java.util.Map;
15 | import java.util.Objects;
16 |
17 | public class ClassOperation {
18 |
19 | private final Map> data = new HashMap<>();
20 |
21 | public void load(File file) throws IOException {
22 | try (FileInputStream fis = new FileInputStream(file);
23 | InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8);
24 | BufferedReader br = new BufferedReader(isr)) {
25 | String line;
26 | ClassItem classItem = null;
27 | _while:
28 | while ((line = br.readLine()) != null) {
29 | if (line.startsWith("#")) continue;
30 |
31 | if (line.matches("^\\s+\\S+$")) {
32 | if (null == classItem || null == classItem.memberOperation) {
33 | invalidLine(line);
34 | } else {
35 | boolean ok = classItem.memberOperation.addLine(line);
36 | if (!ok) {
37 | invalidLine(line);
38 | }
39 | }
40 | } else {
41 | String[] fs = line.split("\\s+");
42 | if (fs.length != 2) {
43 | invalidLine(line);
44 | } else {
45 | String regex = TextUtils.convertToRegex(fs[1]);
46 | String type;
47 | boolean member = false;
48 | switch (fs[0]) {
49 | case "delete":
50 | case "d":
51 | type = "d";
52 | break;
53 | case "ignore":
54 | case "i":
55 | type = "i";
56 | break;
57 | case "merge":
58 | case "m":
59 | member = true;
60 | type = "m";
61 | break;
62 | case "original":
63 | case "o":
64 | type = "o";
65 | break;
66 | case "replace":
67 | case "r":
68 | type = "r";
69 | break;
70 | default:
71 | invalidLine(line);
72 | continue _while;
73 | }
74 | HashSet classItems = data.computeIfAbsent(type, k -> new HashSet<>());
75 | classItem = new ClassItem(regex, member);
76 | classItems.add(classItem);
77 | }
78 | }
79 | }
80 | }
81 | }
82 |
83 | private void invalidLine(String line) {
84 | System.err.printf("ClassOperation:invalidLine {%s}%n", line);
85 | }
86 |
87 | public boolean matchDelete(String className) {
88 | return null != find("d", className);
89 | }
90 |
91 | public boolean matchIgnore(String className) {
92 | return null != find("i", className);
93 | }
94 |
95 | public boolean matchReplace(String className) {
96 | return null != find("r", className);
97 | }
98 |
99 | public MemberOperation matchMerge(String className) {
100 | ClassItem classItem = find("m", className);
101 | if (null != classItem) {
102 | return classItem.memberOperation;
103 | }
104 | return null;
105 | }
106 |
107 | public boolean matchOriginal(String className) {
108 | return null != find("o", className);
109 | }
110 |
111 | private ClassItem find(String type, String className) {
112 | HashSet classItems = data.get(type);
113 | if (null != classItems) {
114 | for (ClassItem classItem : classItems) {
115 | if (classItem.matchClassName(className)) {
116 | return classItem;
117 | }
118 | }
119 | }
120 | return null;
121 | }
122 |
123 | private static class ClassItem {
124 |
125 | private final String regex;
126 | private RegexMatcher regexMatcher;
127 | private MemberOperation memberOperation;
128 |
129 | private ClassItem(String regex, boolean member) {
130 | this.regex = regex;
131 | if (member) {
132 | memberOperation = new MemberOperation();
133 | }
134 | }
135 |
136 | @Override
137 | public boolean equals(Object o) {
138 | if (this == o) return true;
139 | if (o == null || getClass() != o.getClass()) return false;
140 | ClassItem classItem = (ClassItem) o;
141 | return regex.equals(classItem.regex);
142 | }
143 |
144 | @Override
145 | public int hashCode() {
146 | return Objects.hash(regex);
147 | }
148 |
149 | private boolean matchClassName(String className) {
150 | if (null == regexMatcher) regexMatcher = new RegexMatcher(regex);
151 | return regexMatcher.matches(className);
152 | }
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 简介
2 | **plugin**
3 | 用于android gradle项目插件,让其支持smali和java混合开发
4 |
5 | # 使用
6 | [](https://jitpack.io/#Mosect/Android-SmaliPlugin)
7 |
8 | ## 1. 添加jitpack
9 | 在根项目build.gradle文件中,添加仓库:
10 | ```
11 | buildscript {
12 | repositories {
13 | maven { url 'https://jitpack.io' }
14 | }
15 | }
16 |
17 | allprojects {
18 | repositories {
19 | maven { url 'https://jitpack.io' }
20 | }
21 | }
22 | ```
23 |
24 | ## 2. 添加classpath
25 | 在根项目build.gradle文件中,添加classpath:
26 | ```
27 | buildscript {
28 | repositories {
29 | maven { url 'https://jitpack.io' }
30 | }
31 | dependencies {
32 | // 自己的com.android.tools.build:gradle插件,支持3.4.0+
33 | // classpath "com.android.tools.build:gradle:3.4.0"
34 | // Android-SmaliPlugin
35 | classpath 'com.github.Mosect:Android-SmaliPlugin:1.2.0-b1'
36 | }
37 | }
38 | ```
39 |
40 | ## 3. 启用插件
41 | 在Android app模块的build.gradle中,启用插件:
42 | ```
43 | // Android插件
44 | //apply plugin: 'com.android.application'
45 | // 在Android插件之后,启用Android-SmaliPlugin
46 | apply plugin: 'com.mosect.smali.plugin.SmaliPlugin'
47 | ```
48 |
49 | ## 4. 添加smali代码
50 | 在Android app模块目录下,创建src/main/smali/classes文件夹,讲smali文件放进此文件夹即可构建java+smali的apk和aab
51 |
52 | ### a. 多口味smali源码文件夹配置
53 | 默认情况下,在sourceSet目录创建smali/classes文件夹,讲smali放进去即可,如果配置了productFlavors,可以在其sourceSet目录创建smali/classes即可,其源码位置类似java。
54 |
55 | 比如:
56 | * 默认sourceSet smali源码位置:src/main/smali/classes
57 | * debug sourceSet smali源码位置:src/debug/smali/classes
58 | * release sourceSet smali源码位置:src/release/smali/classes
59 | * beta(productFlavors配置) sourceSet smali源码位置:src/beta/smali/classes
60 |
61 |
62 | ### b. 多dex配置
63 | smali/classes的源码编译后存放到classes.dex,如需存放到其他dex,可以将smali源码放在smali/${dex_name},有效的dex_name:classes、classes2、classes3 ... classes99
64 |
65 | ### c. 自定义smali源码目录
66 | 在Android app模块build.gradle文件中:
67 |
68 | ```
69 | android {
70 | sourceSets {
71 | main {
72 | smali {
73 | // 完全使用自己定义的目录
74 | setDirs([file('my-smali/a'), file('my-smali/b')])
75 | // 添加自己定义的目录
76 | addDir(file('my-smali/a'))
77 | // 添加多个目录
78 | addDirs(file('my-smali/a'), file('my-smali/b'))
79 | }
80 | }
81 | }
82 | }
83 | ```
84 |
85 | 其他sourceSet同理
86 |
87 | ### d. 指定smali编译apiLevel
88 | 默认smali apiLevel为15,无特殊需求,一般不用设置此选项
89 | ```
90 | android {
91 | sourceSets {
92 | main {
93 | smali {
94 | apiLevel = 16
95 | }
96 | }
97 | }
98 | }
99 | ```
100 |
101 | ### e. 额外规则:移动类到指定dex:class-position.txt
102 | 如果需要批量或者移动很多类,又不方便调整smali源码目录情况下,可以使用class-position.txt文件来定义类存放位置规则:
103 |
104 | 有两种方式:
105 | 1. 在Android app模块目录下创建class-position.txt文件,或者在某个sourceSet目录下创建,例如src/main/class-position.txt,多个sourceSet都配置,其规则将进行叠加
106 | 2. 在Android app模块build.gradle文件中:
107 | ```
108 | android {
109 | sourceSets {
110 | main {
111 | smali {
112 | // 完全使用自己定义的文件
113 | setPositionFiles([file('my/class-position.txt')])
114 | // 添加单个文件
115 | addPositionFile(file('my/class-position.txt'))
116 | // 添加多个文件
117 | addPositionFiles(file('my/class-position.txt'), file('my2/class-position.txt'))
118 | }
119 | }
120 | }
121 | }
122 | ```
123 |
124 | 文件示例及规则:
125 | ```
126 | # #开头为注释
127 | # 移动类到最后一个dex
128 | move 0:com.mosect.Test end
129 | # 移动类到新的dex
130 | move 1:com.mosect.* new
131 | # 移动类到第二个dex,即classes2.dex
132 | move 2:com.mosect.** 2
133 | # 移动类到主dex,即classes.dex
134 | move 0:com.mosect.Test main
135 | # 删除类
136 | delete 2:com.mosect.MyTest
137 | ```
138 | * 指令规则:move|delete class_path target,move表示移动类,delete表示删除类,delete没有target参数
139 | * 类路径格式:${dex_index}:${class_path},有效的dex_index:0-99;0表示不指定dex,即所有dex中匹配到就指定相应动作。class_path可以包含 \*或者\*\* 来匹配多个类,使用java格式类路径
140 |
141 | ## java+smali编程
142 | 在Android app模块build.gradle中,引用sdk:
143 | ```
144 | dependencies {
145 | implementation 'com.github.Mosect:Android-SmaliSdk:1.2.0'
146 | }
147 | ```
148 | 在包com.mosect.smali.annotation下提供几种注解,此注解不会被打包进dex,用于Android-SmaliPlugin处理,以下为注解说明(按处理优先级):
149 |
150 | ### Copy 注解
151 | 将方法复制成指定名称方法,支持与其他注解混用,此注解只支持方法
152 |
153 | ### Delete 注解
154 | 删除smali和java源码中的类、方法或字段,
155 |
156 | ### Original 注解
157 | 使用smali源码中的类、方法或字段,即忽略java中的类、方法或字段
158 |
159 | ### Replace 注解
160 | 替换smali源码中的类、方法或字段,即只使用java中的类、方法或字段
161 |
162 | ### Merge 注解
163 | 合并java和smali类
164 |
165 | ### Ignore 注解
166 | 忽略java中类、方法或字段,即java中的类、方法或字段不会打包进dex
167 |
168 | java调用smali源码,需要在java中创建对应的类,然后使用@Ignore注解即可。这些注解可以相互配合,组成所需的开发需求
169 |
170 | ## 使用class-operation.txt更改java与smali合并规则
171 | 除了使用注解之外,还可以使用class-operation.txt文件规定合并规则,其配置和class-position.txt类似。
172 |
173 | 有两种方式:
174 | 1. 在Android app模块目录下创建class-operation.txt文件,或者在某个sourceSet目录下创建,例如src/main/class-operation.txt,多个sourceSet都配置,其规则将进行叠加
175 | 2. 在Android app模块build.gradle文件中:
176 | ```
177 | android {
178 | sourceSets {
179 | main {
180 | smali {
181 | // 完全使用自己定义的文件
182 | setOperationFiles([file('my/class-operation.txt')])
183 | // 添加单个文件
184 | addOperationFile(file('my/class-operation.txt'))
185 | // 添加多个文件
186 | addOperationFiles(file('my/class-operation.txt'), file('my2/class-operation.txt'))
187 | }
188 | }
189 | }
190 | }
191 | ```
192 | 文件示例及规则:
193 | ```
194 | # #开头为注释
195 | ignore com.mosect.Test
196 | delete com.mosect.*
197 | original com.mosect.**
198 | replace com.mosect.*
199 | merge com.mosect.Test
200 | ignore field1
201 | delete *
202 | replace field4
203 | ignore method1()
204 | delete method2(String,int)
205 | original method3(java.land.Date,Integer)
206 | ```
207 | * 指令规则:ignore|delete|original|merge|replace class_path
208 |
209 | 其指令和注解一致
210 |
211 | 字段、方法合并,需要在merge之后,并且以空白字符开头,推荐使用\t
212 |
213 | 字段只需写明字段名,支持\*和\*\*;方法需要写明方法名和参数签名,例如:
214 |
215 | met(Ljava.land.String)
216 |
217 | * 类路径格式:class_path。class_path可以包含 \*或者\*\* 来匹配多个类,使用java格式类路径
218 |
219 | # 版本更新记录
220 | ## 1.2.0-b1
221 | ```
222 | 增加Copy注解支持
223 | ```
224 |
225 | ## 1.1.2(稳定)
226 | ```
227 | 引用版本:1.1.2-b2
228 | ```
229 |
230 | ## 1.1.2-b2
231 | ```
232 | 1. 修复了重复类报错问题
233 | 2. 优化dex生成流程
234 | ```
235 |
236 | ## 1.1.1-b10
237 | ```
238 | 1. 兼容 AGP 3.3.0-7.2
239 | 2. 兼容MultiDex和R8
240 | ```
241 |
242 | ## 1.1.1-b1
243 | ```
244 | beta测试版本
245 | 1. 实现基本smali和java混合开发
246 | ```
247 |
248 | # 意见反馈
249 | * 提issue
250 | * 加QQ群聊:624420724
251 | * 个人主页:http://mosect.com
252 |
--------------------------------------------------------------------------------
/plugin/src/test/java/com/mosect/smali/plugin/MainTest.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin;
2 |
3 | import static org.junit.Assert.assertEquals;
4 |
5 | import com.mosect.smali.plugin.dex.ClassOperation;
6 | import com.mosect.smali.plugin.dex.DexDecoder;
7 | import com.mosect.smali.plugin.dex.DexHandler;
8 | import com.mosect.smali.plugin.dex.DexMaker;
9 | import com.mosect.smali.plugin.parser.SmaliBlockNode;
10 | import com.mosect.smali.plugin.parser.SmaliNode;
11 | import com.mosect.smali.plugin.parser.SmaliParseError;
12 | import com.mosect.smali.plugin.parser.SmaliParseResult;
13 | import com.mosect.smali.plugin.parser.SmaliParser;
14 | import com.mosect.smali.plugin.parser.SmaliToken;
15 |
16 | import org.gradle.internal.impldep.com.fasterxml.jackson.annotation.JsonTypeInfo;
17 | import org.junit.Assert;
18 | import org.junit.Test;
19 |
20 | import java.io.ByteArrayOutputStream;
21 | import java.io.File;
22 | import java.io.FileFilter;
23 | import java.io.FileInputStream;
24 | import java.io.IOException;
25 | import java.util.ArrayList;
26 | import java.util.Collections;
27 | import java.util.List;
28 |
29 | public class MainTest {
30 |
31 | @Test
32 | public void testClassOperation() throws Exception {
33 | ClassOperation classOperation = new ClassOperation();
34 | classOperation.load(new File("E:\\Temp\\2022051314\\class-operation.txt"));
35 | Assert.assertTrue(classOperation.matchDelete("com.mosect.del2.A"));
36 | Assert.assertFalse(classOperation.matchDelete("com.mosect.del2.A.B"));
37 | Assert.assertTrue(classOperation.matchDelete("com.mosect.del.A"));
38 | Assert.assertTrue(classOperation.matchDelete("com.mosect.del.A.B"));
39 | Assert.assertFalse(classOperation.matchDelete("com.mosect.Data"));
40 | Assert.assertTrue(classOperation.matchDelete("com.mosect.DeleteTest"));
41 | }
42 |
43 | @Test
44 | public void testParseTokens() throws Exception {
45 | File dir = new File("C:\\Users\\MosectAdmin\\es-file-explorer-4-2-8-7-1");
46 | List smaliFiles = new ArrayList<>(128);
47 | listSmaliFiles(dir, smaliFiles);
48 | for (File file : smaliFiles) {
49 | parseSmaliFile(file);
50 | }
51 | }
52 |
53 | @Test
54 | public void testFile() throws Exception {
55 | File file = new File("C:\\Users\\MosectAdmin\\es-file-explorer-4-2-8-7-1\\smali\\androidx\\mediarouter\\media\\RemoteControlClientCompat$PlaybackInfo.smali");
56 | parseSmaliFile(file);
57 | }
58 |
59 | @Test
60 | public void testDexMaker() throws Exception {
61 | File dir = new File("E:\\Temp\\2022012118\\official-menglar-1.1.28-2022011810\\smali");
62 | List smaliFiles = new ArrayList<>(128);
63 | listSmaliFiles(dir, smaliFiles);
64 | DexMaker dexMaker = new DexMaker(1);
65 | for (File file : smaliFiles) {
66 | dexMaker.addSmaliFile(null, file);
67 | }
68 | File dexFile = new File("build/classes.dex");
69 | boolean ok = dexMaker.makeDex(dexFile);
70 | Assert.assertTrue(ok);
71 | }
72 |
73 | @Test
74 | public void testDexHandler() throws Exception {
75 | DexHandler dexHandler = new DexHandler();
76 | dexHandler.addJavaDexFile(new File("E:\\Temp\\2022012118\\classes.dex"));
77 | dexHandler.addOriginalSourceDir(1, new File("E:\\Temp\\2022012118\\app1\\smali"));
78 | File tempFile = new File("build/temp");
79 | deleteFile(tempFile);
80 | dexHandler.setTempDir(tempFile);
81 | dexHandler.setApiLevel(15);
82 | File dexDir = dexHandler.run();
83 | File[] dexFiles = dexDir.listFiles(new FileFilter() {
84 | @Override
85 | public boolean accept(File file) {
86 | return file.isFile() && file.getName().endsWith(".dex");
87 | }
88 | });
89 | if (null != dexFiles) {
90 | DexDecoder dexDecoder = new DexDecoder();
91 | dexDecoder.setApiLevel(15);
92 | for (File dexFile : dexFiles) {
93 | dexDecoder.addDexFile(dexFile);
94 | }
95 | dexDecoder.decode(new File(dexDir.getParentFile(), "test"));
96 | }
97 | }
98 |
99 | private void deleteFile(File file) {
100 | if (file.isDirectory()) {
101 | File[] files = file.listFiles(file1 -> {
102 | String name = file1.getName();
103 | return !".".equals(name) && !"..".equals(name);
104 | });
105 | if (null != files) {
106 | for (File child : files) {
107 | deleteFile(child);
108 | }
109 | }
110 | file.delete();
111 | } else if (file.isFile()) {
112 | file.delete();
113 | }
114 | }
115 |
116 | private void parseSmaliFile(File file) throws IOException {
117 | // System.out.println("Parse: " + file.getAbsolutePath());
118 | String text = readFile(file);
119 | SmaliParser parser = new SmaliParser();
120 | SmaliParseResult> result = parser.parseTokens(text, 0, text.length());
121 | assertEquals(result.getErrors().size(), 0);
122 | for (SmaliToken token : result.getResult()) {
123 | if ("word".equals(token.getTokenType())) {
124 | boolean valid = token.getText().matches("^[-.a-zA-Z0-9_\\$/;:<>\\[\\]]+$");
125 | if (!valid) {
126 | System.err.println("InvalidWord: " + token.getText());
127 | throw new IllegalStateException("Invalid word");
128 | }
129 | }
130 | }
131 | SmaliParseResult blockResult = parser.parseDocument(text, 0, text.length(), result.getResult());
132 | for (SmaliNode node : blockResult.getResult().getChildren()) {
133 | if ("annotation".equals(node.getType())) {
134 | System.out.println("ExistAnnotation: " + file.getAbsolutePath());
135 | break;
136 | }
137 | }
138 | if (blockResult.getErrors().size() > 0) {
139 | for (SmaliParseError error : blockResult.getErrors()) {
140 | System.err.println(error);
141 | }
142 | throw new IllegalStateException("Exist error");
143 | }
144 | }
145 |
146 | private String readFile(File file) throws IOException {
147 | try (FileInputStream fis = new FileInputStream(file);
148 | ByteArrayOutputStream temp = new ByteArrayOutputStream(512)) {
149 | byte[] buffer = new byte[1024];
150 | int len;
151 | while ((len = fis.read(buffer)) >= 0) {
152 | temp.write(buffer, 0, len);
153 | }
154 | return temp.toString("utf-8");
155 | }
156 | }
157 |
158 | private void listSmaliFiles(File dir, List out) {
159 | File[] files = dir.listFiles(file -> file.isFile() && file.getName().endsWith(".smali"));
160 | if (null != files) {
161 | Collections.addAll(out, files);
162 | }
163 | File[] dirs = dir.listFiles(file -> file.isDirectory() && !".".equals(file.getName()) && !"..".equals(file.getName()));
164 | if (null != dirs) {
165 | for (File childDir : dirs) {
166 | listSmaliFiles(childDir, out);
167 | }
168 | }
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/plugin/src/main/groovy/com/mosect/smali/plugin/SmaliPlugin.groovy:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin
2 |
3 | import com.mosect.smali.plugin.dex.DexHandler
4 | import org.gradle.api.Plugin
5 | import org.gradle.api.Project
6 | import org.gradle.api.Task
7 |
8 | class SmaliPlugin implements Plugin {
9 |
10 | @Override
11 | void apply(Project project) {
12 | project.android.sourceSets.all {
13 | // create smali extension for sourceSet
14 | SmaliExtension extension = it.getExtensions().create('smali', SmaliExtension)
15 | extension.dirs.add(new File(project.projectDir, "src/${it.name}/smali"))
16 | extension.addOperationFile(new File(project.projectDir, "src/${it.name}/class-operation.txt"))
17 | extension.addPositionFile(new File(project.projectDir, "src/${it.name}/class-position.txt"))
18 | if (it.name == 'main') {
19 | extension.addOperationFile(new File(project.projectDir, 'class-operation.txt'))
20 | extension.addPositionFile(new File(project.projectDir, 'class-position.txt'))
21 | }
22 | }
23 |
24 | project.afterEvaluate {
25 | project.android.applicationVariants.all { variant ->
26 | // find dex task
27 | List dexTasks = []
28 | project.tasks.each {
29 | def vn = it.properties.get('variantName')
30 | if (vn == variant.name) {
31 | // variant task
32 | def dexTask = it.name ==~ '^transformDex.*Merger.*$' ||
33 | it.name ==~ '^minify.+WithR8.*$' ||
34 | it.name ==~ '^transformClasses.+WithR8.*$' ||
35 | it.name ==~ '^mergeDex.*$' ||
36 | it.name ==~ '^mergeProjectDex.*$'
37 | if (dexTask) {
38 | // dex task
39 | dexTasks.add(it)
40 | }
41 | }
42 | }
43 | dexTasks.each { task ->
44 | // set task outputs
45 | File tempDir = new File(project.buildDir, "smali/${task.name}")
46 | task.outputs.dir(tempDir)
47 | // set task inputs
48 | variant.sourceSets.each {
49 | SmaliExtension smaliExtension = it.smali
50 | task.inputs.files(smaliExtension.positionFiles)
51 | task.inputs.files(smaliExtension.operationFiles)
52 | smaliExtension.dirs.each {
53 | if (it.isDirectory()) {
54 | task.inputs.dir(it)
55 | }
56 | }
57 | }
58 | task.doLast {
59 | project.delete(tempDir)
60 |
61 | // find dex files
62 | List dexFiles = []
63 | def dexFileInfoList = []
64 | outputs.files.each {
65 | project.fileTree(it).each {
66 | if (it.name ==~ '^classes([0-9]{1,2})?\\.dex$') {
67 | def str = it.name.substring(7, it.name.length() - 4)
68 | int index = 1
69 | if (str.length() > 0) {
70 | index = Integer.parseInt(str)
71 | }
72 | dexFileInfoList.add([
73 | index: index,
74 | file : it
75 | ])
76 | }
77 | }
78 | }
79 | dexFileInfoList.sort(new Comparator() {
80 | @Override
81 | int compare(Object o1, Object o2) {
82 | return o1.index - o2.index
83 | }
84 | })
85 |
86 | dexFileInfoList.each {
87 | dexFiles.add(it.file)
88 | }
89 |
90 | if (dexFiles.isEmpty()) {
91 | System.err.println("DexHandler:skip dex file not found")
92 | return
93 | }
94 |
95 | // exists dex file
96 | DexHandler dexHandler = new DexHandler()
97 | dexHandler.tempDir = tempDir
98 | dexFiles.each {
99 | println("DexHandler:addDexFile: ${it.absolutePath}")
100 | dexHandler.addJavaDexFile(it)
101 | }
102 | // configure dexHandler
103 | int apiLevel = 15
104 | variant.sourceSets.each {
105 | if (null != it.smali.apiLevel) {
106 | apiLevel = it.smali.apiLevel
107 | }
108 |
109 | it.smali.operationFiles.each { File file ->
110 | if (file.exists() && file.isFile()) {
111 | println("DexHandler:addOperationFile: ${file.absolutePath}")
112 | dexHandler.addOperationFile(file)
113 | }
114 | }
115 |
116 | it.smali.positionFiles.each { File file ->
117 | if (file.exists() && file.isFile()) {
118 | println("DexHandler:addPositionFile: ${file.absolutePath}")
119 | dexHandler.addPositionFile(file)
120 | }
121 | }
122 |
123 | // find smali classes directory
124 | it.smali.dirs.each { File dir ->
125 | def dirs = dir.listFiles(new FileFilter() {
126 | @Override
127 | boolean accept(File file) {
128 | return file.isDirectory() && file.getName().matches('^classes([2-9][0-9]?)*$')
129 | }
130 | })
131 | if (dirs) {
132 | dirs.each {
133 | int dexIndex
134 | if (it.name == 'classes') {
135 | dexIndex = 1
136 | } else {
137 | dexIndex = Integer.parseInt(it.name.substring('classes'.length()))
138 | }
139 | println("DexHandler:addSmaliDir: ${it.absolutePath}")
140 | dexHandler.addOriginalSourceDir(dexIndex, it)
141 | }
142 | }
143 | }
144 | }
145 | println("DexHandler:apiLevel ${apiLevel}")
146 | dexHandler.apiLevel = apiLevel
147 | File dexDir = dexFiles[0].parentFile
148 | println("DexHandler:run")
149 | File outDir = dexHandler.run()
150 | println("DexHandler:apply")
151 | // delete original dex
152 | dexFiles.each {
153 | it.delete()
154 | }
155 | project.copy {
156 | from(outDir)
157 | into(dexDir)
158 | }
159 | println("DexHandler:ok")
160 | }
161 | }
162 | }
163 | }
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/dex/SmaliMerger.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.dex;
2 |
3 | import com.mosect.smali.plugin.parser.SmaliAnnotationNode;
4 | import com.mosect.smali.plugin.parser.SmaliBlockNode;
5 | import com.mosect.smali.plugin.parser.SmaliNode;
6 | import com.mosect.smali.plugin.parser.SmaliToken;
7 | import com.mosect.smali.plugin.util.IOUtils;
8 | import com.mosect.smali.plugin.util.TextUtils;
9 |
10 | import java.io.BufferedWriter;
11 | import java.io.File;
12 | import java.io.FileOutputStream;
13 | import java.io.IOException;
14 | import java.io.OutputStreamWriter;
15 | import java.nio.charset.StandardCharsets;
16 | import java.util.ArrayList;
17 | import java.util.Comparator;
18 | import java.util.HashMap;
19 | import java.util.HashSet;
20 | import java.util.List;
21 | import java.util.Map;
22 | import java.util.Objects;
23 | import java.util.Set;
24 |
25 | /**
26 | * 将dex和smali源码合并
27 | */
28 | public class SmaliMerger {
29 |
30 | private final static String COPY = "com.mosect.smali.annotation.Copy";
31 | private final static String DELETE = "com.mosect.smali.annotation.Delete";
32 | private final static String IGNORE = "com.mosect.smali.annotation.Ignore";
33 | private final static String MERGE = "com.mosect.smali.annotation.Merge";
34 | private final static String ORIGINAL = "com.mosect.smali.annotation.Original";
35 | private final static String REPLACE = "com.mosect.smali.annotation.Replace";
36 |
37 | private final SimpleSmaliParser parser = new SimpleSmaliParser();
38 | private ClassesSource javaSource;
39 | private File tempDir;
40 | private final Map dexMakerMap = new HashMap<>();
41 | private final ClassOperation classOperation = new ClassOperation();
42 | private final ClassPosition classPosition = new ClassPosition();
43 |
44 | public void setJavaSource(ClassesSource javaSource) {
45 | this.javaSource = javaSource;
46 | }
47 |
48 | public void setTempDir(File tempDir) {
49 | this.tempDir = tempDir;
50 | }
51 |
52 | public void addDexMaker(DexMaker dexMaker) {
53 | DexMaker old = dexMakerMap.put(dexMaker.getIndex(), dexMaker);
54 | if (null != old) {
55 | throw new IllegalArgumentException("Exist DexMaker: " + dexMaker.getName());
56 | }
57 | }
58 |
59 | public void addOperationFile(File file) throws IOException {
60 | classOperation.load(file);
61 | }
62 |
63 | public void addPositionFile(File file) throws IOException {
64 | classPosition.load(file);
65 | }
66 |
67 | public List merge() throws IOException, SmaliException {
68 | if (null == javaSource) {
69 | throw new IllegalArgumentException("javaSource not set");
70 | }
71 | if (null == tempDir) {
72 | throw new IllegalArgumentException("tempDir not set");
73 | }
74 | if (dexMakerMap.isEmpty()) {
75 | throw new IllegalArgumentException("dexMaker not set");
76 | }
77 |
78 | Map javaSmaliFiles = loadSmaliFiles(javaSource);
79 | List dexMakerList = new ArrayList<>(dexMakerMap.values());
80 | dexMakerList.sort(Comparator.comparing(DexMaker::getIndex));
81 |
82 | for (Map.Entry entry : javaSmaliFiles.entrySet()) {
83 | String className = entry.getKey();
84 | if (className.startsWith("com.mosect.smali.annotation.")) {
85 | continue;
86 | }
87 |
88 | SmaliBlockNode javaBlockNode = parser.parse(entry.getValue());
89 | Map javaAnnotations = getAnnotations(javaBlockNode);
90 | if (classOperation.matchDelete(className) || javaAnnotations.containsKey(DELETE)) {
91 | // 删除
92 | for (DexMaker dexMaker : dexMakerList) {
93 | dexMaker.removeSmaliFile(className);
94 | }
95 | } else if (classOperation.matchOriginal(className) || javaAnnotations.containsKey(ORIGINAL)) {
96 | // 使用原本smali
97 | DexMaker dexMaker = findDexMaker(dexMakerList, className);
98 | if (null == dexMaker) {
99 | throw new SmaliException("MissingClass: " + className);
100 | }
101 | } else if (classOperation.matchReplace(className) || javaAnnotations.containsKey(REPLACE)) {
102 | // 替换
103 | File file = writeSmaliFile(tempDir, className, javaBlockNode);
104 | DexMaker dexMaker = findDexMaker(dexMakerList, className);
105 | if (null == dexMaker) {
106 | dexMaker = dexMakerList.get(0);
107 | }
108 | dexMaker.addSmaliFile(className, file);
109 | } else {
110 | MemberOperation memberOperation = classOperation.matchMerge(className);
111 | if (null != memberOperation || javaAnnotations.containsKey(MERGE)) {
112 | // 合并
113 | DexMaker dexMaker = findDexMaker(dexMakerList, className);
114 | if (null == dexMaker) {
115 | throw new SmaliException("MissingClass: " + className);
116 | }
117 | File originalFile = dexMaker.getSmaliFile(className);
118 | SmaliBlockNode originalBlockNode = parser.parse(originalFile);
119 | // 合并字段
120 | mergeSmali(memberOperation, javaBlockNode, originalBlockNode, "field");
121 | // 合并方法
122 | mergeSmali(memberOperation, javaBlockNode, originalBlockNode, "method");
123 | File file = writeSmaliFile(tempDir, className, originalBlockNode);
124 | dexMaker.addSmaliFile(className, file);
125 | } else if (!classOperation.matchIgnore(className) && !javaAnnotations.containsKey(IGNORE)) {
126 | // 非忽略类
127 | File file = writeSmaliFile(tempDir, className, javaBlockNode);
128 | DexMaker dexMaker = dexMakerList.get(0);
129 | dexMaker.addSmaliFile(className, file);
130 | }
131 | }
132 | }
133 |
134 | // change class dex index
135 | DexMaker newDexMaker = null;
136 | DexMaker endDexMaker = dexMakerList.get(dexMakerList.size() - 1);
137 | int newDexMakerIndex = endDexMaker.getIndex() + 1;
138 | for (DexMaker dexMaker : dexMakerList) {
139 | Set> classSet = new HashSet<>(dexMaker.allClasses());
140 | for (Map.Entry classItem : classSet) {
141 | String className = classItem.getKey();
142 | File smaliFile = classItem.getValue();
143 | ClassPosition.PositionItem pi = classPosition.find(dexMaker.getIndex(), className);
144 | if (null != pi) {
145 | if (pi.getToIndex() == ClassPosition.INDEX_NEW) {
146 | // Move class to new dex
147 | if (null == newDexMaker) {
148 | newDexMaker = new DexMaker(newDexMakerIndex);
149 | dexMakerMap.put(newDexMakerIndex, newDexMaker);
150 | }
151 | newDexMaker.addSmaliFile(className, smaliFile);
152 | dexMaker.removeSmaliFile(className);
153 | } else if (pi.getToIndex() == ClassPosition.INDEX_END) {
154 | // Move class to end dex
155 | if (dexMaker.getIndex() != endDexMaker.getIndex()) {
156 | endDexMaker.addSmaliFile(className, smaliFile);
157 | dexMaker.removeSmaliFile(className);
158 | }
159 | } else if (pi.getToIndex() != dexMaker.getIndex()) {
160 | // Move class to particular dex
161 | DexMaker toDexMaker = dexMakerMap.get(pi.getToIndex());
162 | if (null == toDexMaker) {
163 | toDexMaker = new DexMaker(pi.getToIndex());
164 | dexMakerMap.put(pi.getToIndex(), toDexMaker);
165 | }
166 | toDexMaker.addSmaliFile(className, smaliFile);
167 | dexMaker.removeSmaliFile(className);
168 | }
169 | }
170 | }
171 | }
172 |
173 | dexMakerList = new ArrayList<>(dexMakerMap.values());
174 | dexMakerList.sort(Comparator.comparing(DexMaker::getIndex));
175 | return dexMakerList;
176 | }
177 |
178 | private void mergeSmali(
179 | MemberOperation memberOperation,
180 | SmaliBlockNode javaBlockNode,
181 | SmaliBlockNode originalBlockNode,
182 | String type) throws SmaliException {
183 | if (javaBlockNode.getChildCount() > 0) {
184 | List deleteNodes = new ArrayList<>();
185 | List addNodes = new ArrayList<>();
186 | for (SmaliNode childNode : javaBlockNode.getChildren()) {
187 | if (Objects.equals(childNode.getType(), type)) {
188 | SmaliBlockNode childBlockNode = (SmaliBlockNode) childNode;
189 | String id = childBlockNode.getId();
190 | if (TextUtils.isEmpty(id)) {
191 | continue;
192 | }
193 | Map annotations = getAnnotations(childBlockNode);
194 | if ("method".equals(type) && annotations.containsKey(COPY)) {
195 | // 执行复制操作
196 | SmaliAnnotationNode copyAnnotation = annotations.get(COPY);
197 | CopyValueMatcher matcher = new CopyValueMatcher();
198 | if (copyAnnotation.match(matcher)) {
199 | String dstName = matcher.getValue();
200 | SmaliBlockNode blockNode = findBlockNode(originalBlockNode, type, id);
201 | SmaliBlockNode copy = blockNode.copy();
202 | MethodNameMatcher methodNameMatcher = new MethodNameMatcher();
203 | if (copy.match(methodNameMatcher)) {
204 | SmaliToken nameToken = new SmaliToken(dstName, "word");
205 | copy.getChildren().set(methodNameMatcher.getNameNodeIndex(), nameToken);
206 | addNodes.add(copy);
207 | }
208 | }
209 | }
210 | if ((null != memberOperation && memberOperation.matchDelete(type, id)) || annotations.containsKey(DELETE)) {
211 | SmaliBlockNode blockNode = findBlockNode(originalBlockNode, type, id);
212 | deleteNodes.add(blockNode);
213 | } else if ((null != memberOperation && memberOperation.matchOriginal(type, id)) || annotations.containsKey(ORIGINAL)) {
214 | findBlockNode(originalBlockNode, type, id);
215 | } else if ((null != memberOperation && memberOperation.matchReplace(type, id)) || annotations.containsKey(REPLACE)) {
216 | SmaliBlockNode blockNode = findBlockNode(originalBlockNode, type, id);
217 | int index = originalBlockNode.getChildren().indexOf(blockNode);
218 | originalBlockNode.getChildren().set(index, childBlockNode);
219 | } else if ((null != memberOperation && !memberOperation.matchIgnore(type, id)) && !annotations.containsKey(IGNORE)) {
220 | // 非忽略,直接添加
221 | originalBlockNode.getChildren().add(childBlockNode);
222 | originalBlockNode.getChildren().add(SmaliToken.line());
223 | }
224 | }
225 | }
226 | if (!deleteNodes.isEmpty()) {
227 | originalBlockNode.getChildren().removeAll(deleteNodes);
228 | }
229 | if (!addNodes.isEmpty()) {
230 | originalBlockNode.getChildren().addAll(addNodes);
231 | }
232 | }
233 | }
234 |
235 | private SmaliBlockNode findBlockNode(SmaliBlockNode target, String type, String id) throws SmaliException {
236 | if (target.getChildCount() > 0) {
237 | for (SmaliNode childNode : target.getChildren()) {
238 | if (Objects.equals(type, childNode.getType())) {
239 | SmaliBlockNode childBlockNode = (SmaliBlockNode) childNode;
240 | if (Objects.equals(id, childBlockNode.getId())) {
241 | return childBlockNode;
242 | }
243 | }
244 | }
245 | }
246 | throw new SmaliException("InvalidSmali: Missing " + type + ":" + id);
247 | }
248 |
249 | private DexMaker findDexMaker(List dexMakerList, String className) {
250 | for (DexMaker dexMaker : dexMakerList) {
251 | File file = dexMaker.getSmaliFile(className);
252 | if (null != file) {
253 | return dexMaker;
254 | }
255 | }
256 | return null;
257 | }
258 |
259 | private File writeSmaliFile(File dir, String className, SmaliBlockNode blockNode) throws IOException {
260 | removeAnnotations(blockNode);
261 | File file = ClassesSource.getSmaliFile(dir, className);
262 | IOUtils.initParent(file);
263 | StringBuilder builder = new StringBuilder();
264 | blockNode.append(builder);
265 | try (FileOutputStream fos = new FileOutputStream(file);
266 | OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);
267 | BufferedWriter bw = new BufferedWriter(osw)) {
268 | bw.write(builder.toString());
269 | }
270 | return file;
271 | }
272 |
273 | protected Map getAnnotations(SmaliBlockNode blockNode) {
274 | Map result = new HashMap<>();
275 | if (blockNode.getChildCount() > 0) {
276 | for (SmaliNode node : blockNode.getChildren()) {
277 | if ("annotation".equals(node.getType())) {
278 | SmaliAnnotationNode annotationNode = (SmaliAnnotationNode) node;
279 | String className = annotationNode.getClassName();
280 | if (!TextUtils.isEmpty(className) && className.startsWith("com.mosect.smali.annotation.")) {
281 | result.put(className, annotationNode);
282 | }
283 | }
284 | }
285 | }
286 | return result;
287 | }
288 |
289 | protected void removeAnnotations(SmaliBlockNode blockNode) {
290 | if (blockNode.getChildCount() > 0) {
291 | List nodes = new ArrayList<>();
292 | for (SmaliNode node : blockNode.getChildren()) {
293 | switch (node.getType()) {
294 | case "annotation":
295 | SmaliAnnotationNode annotationNode = (SmaliAnnotationNode) node;
296 | String className = annotationNode.getClassName();
297 | if (!TextUtils.isEmpty(className) && className.startsWith("com.mosect.smali.annotation.")) {
298 | nodes.add(annotationNode);
299 | }
300 | break;
301 | case "field":
302 | case "method":
303 | removeAnnotations((SmaliBlockNode) node);
304 | break;
305 | }
306 | }
307 | blockNode.getChildren().removeAll(nodes);
308 | }
309 | }
310 |
311 | private Map loadSmaliFiles(ClassesSource source) throws IOException {
312 | List list = source.listAll();
313 | Map map = new HashMap<>();
314 | for (ClassesSource.SmaliFileInfo fileInfo : list) {
315 | File old = map.put(fileInfo.getClassName(), fileInfo.getFile());
316 | if (null != old) {
317 | System.err.printf("ReplaceClasses: %s >>> %s%n", fileInfo.getFile(), old);
318 | }
319 | }
320 | return map;
321 | }
322 | }
323 |
--------------------------------------------------------------------------------
/plugin/src/main/java/com/mosect/smali/plugin/parser/SmaliParser.java:
--------------------------------------------------------------------------------
1 | package com.mosect.smali.plugin.parser;
2 |
3 | import java.io.ByteArrayOutputStream;
4 | import java.io.File;
5 | import java.io.FileInputStream;
6 | import java.io.IOException;
7 | import java.nio.charset.Charset;
8 | import java.nio.charset.StandardCharsets;
9 | import java.util.ArrayList;
10 | import java.util.Collections;
11 | import java.util.List;
12 |
13 | public class SmaliParser {
14 |
15 | private final byte[] buffer = new byte[1024];
16 |
17 | public SmaliParseResult parseFileWithUtf8(File file) throws IOException {
18 | return parseFile(file, StandardCharsets.UTF_8);
19 | }
20 |
21 | public SmaliParseResult parseFile(File file, Charset charset) throws IOException {
22 | try (FileInputStream fis = new FileInputStream(file);
23 | ByteArrayOutputStream temp = new ByteArrayOutputStream(512)) {
24 | int len;
25 | while ((len = fis.read(buffer)) >= 0) {
26 | if (len > 0) temp.write(buffer, 0, len);
27 | }
28 | String text = temp.toString(charset.name());
29 | SmaliParseResult> tokensResult = parseTokens(text, 0, text.length());
30 | if (!tokensResult.getErrors().isEmpty()) {
31 | return new SmaliParseResult<>(null, tokensResult.getErrors());
32 | }
33 |
34 | return parseDocument(text, 0, text.length(), tokensResult.getResult());
35 | }
36 | }
37 |
38 | public SmaliParseResult> parseTokens(CharSequence text, int start, int end) {
39 | List result = new ArrayList<>();
40 | List errors = new ArrayList<>();
41 |
42 | int offset = start;
43 | while (offset < end) {
44 | if (match(text, offset, end, "#")) {
45 | // 注释
46 | int commentEnd = findCommentEnd(text, offset + 1, end);
47 | String comment = text.subSequence(offset, commentEnd).toString();
48 | result.add(new SmaliToken(comment, "comment"));
49 | offset = commentEnd;
50 | } else if (match(text, offset, end, "\r\n")) {
51 | // 换行
52 | result.add(SmaliToken.lineCrlf());
53 | offset += 2;
54 | } else if (match(text, offset, end, "..")) {
55 | // .. 运算符
56 | result.add(new SmaliToken("..", "symbol"));
57 | offset += 2;
58 | } else if (match(text, offset, end, "\"")) {
59 | // 字符串
60 | int stringEnd = findStringEnd(text, offset + 1, end, errors);
61 | String string = text.subSequence(offset, stringEnd).toString();
62 | result.add(new SmaliToken(string, "string"));
63 | offset = stringEnd;
64 | } else if (match(text, offset, end, "'")) {
65 | // 字符
66 | int charEnd = findCharEnd(text, offset + 1, end, errors);
67 | String cs = text.subSequence(offset, charEnd).toString();
68 | result.add(new SmaliToken(cs, "char"));
69 | offset = charEnd;
70 | } else {
71 | char ch = text.charAt(offset);
72 | switch (ch) {
73 | case '\r':
74 | result.add(SmaliToken.lineCr());
75 | ++offset;
76 | break;
77 | case '\n':
78 | result.add(SmaliToken.lineLf());
79 | ++offset;
80 | break;
81 | case '=':
82 | case '(':
83 | case ')':
84 | case '{':
85 | case '}':
86 | case ',':
87 | case ':':
88 | result.add(new SmaliToken(String.valueOf(ch), "symbol"));
89 | ++offset;
90 | break;
91 | case '\t':
92 | case ' ':
93 | result.add(new SmaliToken(String.valueOf(ch), "whitespace"));
94 | ++offset;
95 | break;
96 | default:
97 | int wordEnd = findWordEnd(text, offset, end);
98 | String word = text.subSequence(offset, wordEnd).toString();
99 | if (word.startsWith(".")) {
100 | result.add(new SmaliToken(word, "block"));
101 | } else {
102 | result.add(new SmaliToken(word, "word"));
103 | }
104 | offset = wordEnd;
105 | break;
106 | }
107 | }
108 | }
109 |
110 | return new SmaliParseResult<>(result, errors);
111 | }
112 |
113 | public SmaliParseResult parseDocument(CharSequence text, int start, int end, List tokens) {
114 | List errors = new ArrayList<>();
115 |
116 | // 整理SmaliEndBlock
117 | List nodes = new ArrayList<>(tokens);
118 | SmaliBlockNode rootBlockNode = new SmaliBlockNode();
119 | int offset = 0;
120 | while (offset < nodes.size()) {
121 | SmaliToken token = (SmaliToken) nodes.get(offset);
122 | if ("block".equals(token.getTokenType()) && ".end".equals(token.getText())) {
123 | int wordIndex = findWordToken(nodes, offset + 1);
124 | SmaliEndNode endNode = new SmaliEndNode();
125 | if (wordIndex < 0) {
126 | endNode.getChildren().add(token);
127 | rootBlockNode.getChildren().add(endNode);
128 | errors.add(new SmaliParseError("END:Missing end word", "Missing end word", rootBlockNode.length()));
129 | ++offset;
130 | } else {
131 | for (int i = offset; i <= wordIndex; i++) {
132 | endNode.getChildren().add(nodes.get(i));
133 | }
134 | rootBlockNode.getChildren().add(endNode);
135 | offset = wordIndex + 1;
136 | }
137 | } else {
138 | rootBlockNode.getChildren().add(token);
139 | ++offset;
140 | }
141 | }
142 |
143 | // 创建BlockNode
144 | List rootNodes = rootBlockNode.getChildren();
145 | rootBlockNode.setChildren(new ArrayList<>());
146 | generateBlockNodesWithEnd(rootNodes, 0, rootNodes.size(), rootBlockNode);
147 | generateBlockNodesWithStart(rootBlockNode);
148 |
149 | return new SmaliParseResult<>(rootBlockNode, errors);
150 | }
151 |
152 | protected void generateBlockNodesWithEnd(List nodes, int start, int end, SmaliNode out) {
153 | int offset = end - 1;
154 | List children = new ArrayList<>();
155 | while (offset >= start) {
156 | SmaliNode node = nodes.get(offset);
157 | if ("end".equals(node.getType())) {
158 | SmaliEndNode endNode = (SmaliEndNode) node;
159 | String blockName = endNode.getBlockName();
160 | if (null == blockName) {
161 | children.add(node);
162 | --offset;
163 | continue;
164 | }
165 |
166 | int blockStartIndex = findBlockToken(nodes, start, offset, blockName);
167 | SmaliBlockNode nextBlock;
168 | if (blockStartIndex < 0) {
169 | nextBlock = createBlockNode("");
170 | generateBlockNodesWithEnd(nodes, start, offset, nextBlock);
171 | } else {
172 | SmaliToken token = (SmaliToken) nodes.get(blockStartIndex);
173 | nextBlock = createBlockNode(token.getText());
174 | nextBlock.getChildren().add(token);
175 | generateBlockNodesWithEnd(nodes, blockStartIndex + 1, offset, nextBlock);
176 | }
177 |
178 | nextBlock.getChildren().add(nodes.get(offset));
179 | children.add(nextBlock);
180 | offset = blockStartIndex - 1;
181 | } else {
182 | children.add(node);
183 | --offset;
184 | }
185 | }
186 | Collections.reverse(children);
187 | out.getChildren().addAll(children);
188 | }
189 |
190 | protected void generateBlockNodesWithStart(SmaliBlockNode blockNode) {
191 | List nodes = blockNode.getChildren();
192 | List children = new ArrayList<>(nodes.size());
193 | int offset = 0;
194 | while (offset < nodes.size()) {
195 | SmaliNode node = nodes.get(offset);
196 | if ("block".equals(node.getType())) {
197 | SmaliBlockNode nextBlock = (SmaliBlockNode) node;
198 | generateBlockNodesWithStart(nextBlock);
199 | children.add(nextBlock);
200 | ++offset;
201 | } else if ("token".equals(node.getType())) {
202 | SmaliToken token = (SmaliToken) node;
203 | if ("block".equals(token.getTokenType())) {
204 | int blockEnd = findBlockEnd(nodes, offset + 1);
205 | SmaliBlockNode nextBlock = createBlockNode(token.getText());
206 | for (int i = offset; i < blockEnd; i++) {
207 | nextBlock.getChildren().add(nodes.get(i));
208 | }
209 | children.add(nextBlock);
210 | offset = blockEnd;
211 | } else {
212 | children.add(token);
213 | ++offset;
214 | }
215 | } else {
216 | children.add(node);
217 | ++offset;
218 | }
219 | }
220 | blockNode.setChildren(children);
221 | }
222 |
223 | protected int findBlockEnd(List nodes, int start) {
224 | int validIndex = -1;
225 | _for:
226 | for (int i = start; i < nodes.size(); i++) {
227 | SmaliNode node = nodes.get(i);
228 | if ("token".equals(node.getType())) {
229 | SmaliToken token = (SmaliToken) node;
230 | switch (token.getTokenType()) {
231 | case "whitespace":
232 | case "comment":
233 | break;
234 | case "block":
235 | case "line.cr":
236 | case "line.crlf":
237 | case "line.lf":
238 | break _for;
239 | default:
240 | validIndex = i;
241 | break;
242 | }
243 | } else {
244 | validIndex = i;
245 | }
246 | }
247 | if (validIndex >= 0) return validIndex + 1;
248 | return nodes.size();
249 | }
250 |
251 | protected SmaliBlockNode createBlockNode(String type) {
252 | SmaliBlockNode blockNode;
253 | switch (type) {
254 | case ".class":
255 | blockNode = new SmaliClassNode();
256 | break;
257 | case ".method":
258 | blockNode = new SmaliMethodNode();
259 | break;
260 | case ".field":
261 | blockNode = new SmaliFieldNode();
262 | break;
263 | case ".annotation":
264 | blockNode = new SmaliAnnotationNode();
265 | break;
266 | default:
267 | blockNode = new SmaliBlockNode();
268 | break;
269 | }
270 | return blockNode;
271 | }
272 |
273 | protected int findBlockToken(List nodes, int start, int end, String blockName) {
274 | String startTag = "." + blockName;
275 | for (int i = end - 1; i >= start; i--) {
276 | SmaliNode node = nodes.get(i);
277 | if ("token".equals(node.getType())) {
278 | SmaliToken token = (SmaliToken) node;
279 | if ("block".equals(token.getTokenType())) {
280 | if (startTag.equals(token.getText())) {
281 | return i;
282 | }
283 | }
284 | }
285 | }
286 | return -1;
287 | }
288 |
289 | protected int findWordToken(List nodes, int start) {
290 | for (int i = start; i < nodes.size(); i++) {
291 | SmaliNode node = nodes.get(i);
292 | if ("token".equals(node.getType())) {
293 | SmaliToken token = (SmaliToken) node;
294 | if ("word".equals(token.getTokenType())) {
295 | return i;
296 | }
297 | }
298 | }
299 | return -1;
300 | }
301 |
302 | protected int findCommentEnd(CharSequence text, int start, int end) {
303 | for (int i = start; i < end; i++) {
304 | if (matchLine(text, i, end)) return i;
305 | }
306 | return end;
307 | }
308 |
309 | protected int findStringEnd(CharSequence text, int start, int end, List outErrors) {
310 | int offset = start;
311 | while (offset < end) {
312 | if (matchLine(text, offset, end)) {
313 | outErrors.add(new SmaliParseError("STRING:UNEXPECTED_LINE_CHAR", "Unexpected line char", offset));
314 | return offset;
315 | } else if (match(text, offset, end, "\"")) {
316 | // 字符串结束
317 | return offset + 1;
318 | }
319 | offset = nextCharIndex(text, offset, end, outErrors);
320 | }
321 | outErrors.add(new SmaliParseError("STRING:MISSING_END", "Missing string end", end));
322 | return end;
323 | }
324 |
325 | protected int findCharEnd(CharSequence text, int start, int end, List outErrors) {
326 | int offset = start;
327 | int charCount = 0;
328 | while (offset < end) {
329 | if (matchLine(text, offset, end)) {
330 | outErrors.add(new SmaliParseError("CHAR:UNEXPECTED_LINE_CHAR", "Unexpected line char", offset));
331 | return offset;
332 | } else if (match(text, offset, end, "'")) {
333 | // 字符结束
334 | if (charCount == 0) {
335 | outErrors.add(new SmaliParseError("CHAR:MISSING_CONTENT", "Missing char content", start + 1));
336 | } else if (charCount > 1) {
337 | outErrors.add(new SmaliParseError("CHAR:MULTIPLE_CHARS", "Multiple chars", start + 1));
338 | }
339 | return offset + 1;
340 | }
341 | offset = nextCharIndex(text, offset, end, outErrors);
342 | ++charCount;
343 | }
344 | outErrors.add(new SmaliParseError("CHAR:MISSING_END", "Missing char end", end));
345 | return end;
346 | }
347 |
348 | protected boolean matchLine(CharSequence text, int start, int end) {
349 | return match(text, start, end, "\r") || match(text, start, end, "\n");
350 | }
351 |
352 | protected int nextCharIndex(CharSequence text, int start, int end, List outErrors) {
353 | if (match(text, start, end, "\\u")) {
354 | // 16进制字符
355 | int offset = start + 2;
356 | for (int i = 0; i < 4; i++) {
357 | int index = offset + i;
358 | if (index < end) {
359 | char ch = text.charAt(index);
360 | boolean hex = (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F');
361 | if (!hex) {
362 | SmaliParseError error = new SmaliParseError("CHAR:INVALID_HEX_CHAR", "Invalid hex char", index);
363 | outErrors.add(error);
364 | return index;
365 | }
366 | } else {
367 | SmaliParseError error = new SmaliParseError("CHAR:INVALID_HEX_CHAR_LENGTH", "Invalid hex char length", index);
368 | outErrors.add(error);
369 | return index;
370 | }
371 | }
372 | return offset + 4;
373 | } else if (match(text, start, end, "\\")) {
374 | // 转义
375 | int offset = start + 1;
376 | int size = 0;
377 | boolean octEscape = false; // 8进制转义
378 | for (int i = 0; i < 3; i++) {
379 | int index = offset + i;
380 | if (index < end) {
381 | ++size;
382 | char ch = text.charAt(index);
383 | if (size == 1) {
384 | octEscape = ch >= '0' && ch <= '7';
385 | } else {
386 | if (octEscape) {
387 | // 8进制转义
388 | boolean oct = ch >= '0' && ch <= '7';
389 | if (!oct) {
390 | return index;
391 | }
392 | } else {
393 | // 非8进制转义
394 | return index;
395 | }
396 | }
397 | } else {
398 | if (size == 0) {
399 | SmaliParseError error = new SmaliParseError("CHAR:INVALID_ESCAPE_CHAR_LENGTH", "Invalid hex char length", index);
400 | outErrors.add(error);
401 | }
402 | return index;
403 | }
404 | }
405 | return offset + 3;
406 | }
407 | return start + 1;
408 | }
409 |
410 | protected int findWordEnd(CharSequence text, int start, int end) {
411 | for (int i = start; i < end; i++) {
412 | char ch = text.charAt(i);
413 | switch (ch) {
414 | case '\r':
415 | case '\n':
416 | case '\t':
417 | case ' ':
418 | case '=':
419 | case '(':
420 | case ')':
421 | case '{':
422 | case '}':
423 | case ',':
424 | case ':':
425 | return i;
426 | }
427 | }
428 | return end;
429 | }
430 |
431 | protected boolean match(CharSequence text, int offset, int end, String target) {
432 | if (end - offset >= target.length()) {
433 | for (int i = 0; i < target.length(); i++) {
434 | if (target.charAt(i) != text.charAt(offset + i)) {
435 | return false;
436 | }
437 | }
438 | return true;
439 | }
440 | return false;
441 | }
442 | }
443 |
--------------------------------------------------------------------------------