├── 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 | 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 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | 39 | 40 | 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/v/Mosect/Android-SmaliPlugin.svg)](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 | --------------------------------------------------------------------------------