auto copy original apk to assets
This commit is contained in:
parent
2c80821646
commit
581aaf74f7
|
|
@ -1,309 +1,315 @@
|
|||
package com.storm.wind.xpatch;
|
||||
|
||||
import com.storm.wind.xpatch.base.BaseCommand;
|
||||
import com.storm.wind.xpatch.task.ApkModifyTask;
|
||||
import com.storm.wind.xpatch.task.BuildAndSignApkTask;
|
||||
import com.storm.wind.xpatch.task.SaveApkSignatureTask;
|
||||
import com.storm.wind.xpatch.task.SaveOriginalApplicationNameTask;
|
||||
import com.storm.wind.xpatch.task.SoAndDexCopyTask;
|
||||
import com.storm.wind.xpatch.util.FileUtils;
|
||||
import com.storm.wind.xpatch.util.ManifestParser;
|
||||
import com.wind.meditor.core.FileProcesser;
|
||||
import com.wind.meditor.property.AttributeItem;
|
||||
import com.wind.meditor.property.ModificationProperty;
|
||||
import com.wind.meditor.utils.NodeValue;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class MainCommand extends BaseCommand {
|
||||
|
||||
private String apkPath;
|
||||
|
||||
private String unzipApkFilePath;
|
||||
|
||||
@Opt(opt = "o", longOpt = "output", description = "output .apk file, default is " +
|
||||
"$source_apk_dir/[file-name]-xposed-signed.apk", argName = "out-apk-file")
|
||||
private String output; // 输出的apk文件的目录以及名称
|
||||
|
||||
@Opt(opt = "f", longOpt = "force", hasArg = false, description = "force overwrite")
|
||||
private boolean forceOverwrite = false;
|
||||
|
||||
@Opt(opt = "k", longOpt = "keep", hasArg = false, description = "not delete the jar file " +
|
||||
"that is changed by dex2jar and the apk zip files")
|
||||
private boolean keepBuildFiles = false;
|
||||
|
||||
@Opt(opt = "l", longOpt = "log", hasArg = false, description = "show all the debug logs")
|
||||
private boolean showAllLogs = false;
|
||||
|
||||
@Opt(opt = "c", longOpt = "crach", hasArg = false,
|
||||
description = "disable craching the apk's signature.")
|
||||
private boolean disableCrackSignature = false;
|
||||
|
||||
@Opt(opt = "xm", longOpt = "xposed-modules", description = "the xposed module files to be packaged into the apk, " +
|
||||
"multi files should be seperated by :(mac) or ;(win) ", argName = "xposed module file path")
|
||||
private String xposedModules;
|
||||
|
||||
// 使用dex文件中插入代码的方式修改apk,而不是默认的修改Manifest中Application name的方式
|
||||
@Opt(opt = "dex", longOpt = "dex", hasArg = false, description = "insert code into the dex file, not modify manifest application name attribute")
|
||||
private boolean dexModificationMode = false;
|
||||
|
||||
@Opt(opt = "pkg", longOpt = "packageName", description = "modify the apk package name", argName = "new package name")
|
||||
private String newPackageName;
|
||||
|
||||
@Opt(opt = "d", longOpt = "debuggable", description = "set 1 to make the app debuggable = true, " +
|
||||
"set 0 to make the app debuggable = false", argName = "0 or 1")
|
||||
private int debuggable = -1; // 0: debuggable = false 1: debuggable = true
|
||||
|
||||
@Opt(opt = "vc", longOpt = "versionCode", description = "set the app version code",
|
||||
argName = "new-version-code")
|
||||
private int versionCode;
|
||||
|
||||
@Opt(opt = "vn", longOpt = "versionName", description = "set the app version name",
|
||||
argName = "new-version-name")
|
||||
private String versionName;
|
||||
|
||||
@Opt(opt = "w", longOpt = "whale", hasArg = false, description = "Change hook framework to Lody's whale")
|
||||
private boolean useWhaleHookFramework = false; // 是否使用whale hook框架,默认使用的是SandHook
|
||||
|
||||
// 原来apk中dex文件的数量
|
||||
private int dexFileCount = 0;
|
||||
|
||||
private static final String UNZIP_APK_FILE_NAME = "apk-unzip-files";
|
||||
|
||||
private static final String PROXY_APPLICATION_NAME = "com.wind.xpatch.proxy.XpatchProxyApplication";
|
||||
|
||||
private List<Runnable> mXpatchTasks = new ArrayList<>();
|
||||
|
||||
public static void main(String... args) {
|
||||
new MainCommand().doMain(args);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doCommandLine() {
|
||||
if (remainingArgs.length != 1) {
|
||||
if (remainingArgs.length == 0) {
|
||||
System.out.println("Please choose one apk file you want to process. ");
|
||||
}
|
||||
if (remainingArgs.length > 1) {
|
||||
System.out.println("This tool can only used with one apk file.");
|
||||
}
|
||||
usage();
|
||||
return;
|
||||
}
|
||||
|
||||
apkPath = remainingArgs[0];
|
||||
|
||||
File srcApkFile = new File(apkPath);
|
||||
|
||||
if (!srcApkFile.exists()) {
|
||||
System.out.println(" The source apk file not exsit, please choose another one. " +
|
||||
"current apk file is = " + apkPath);
|
||||
return;
|
||||
}
|
||||
|
||||
String currentDir = new File(".").getAbsolutePath(); // 当前命令行所在的目录
|
||||
if (showAllLogs) {
|
||||
System.out.println(" currentDir = " + currentDir + " \n apkPath = " + apkPath);
|
||||
}
|
||||
|
||||
if (output == null || output.length() == 0) {
|
||||
output = getBaseName(apkPath) + "-xposed-signed.apk";
|
||||
}
|
||||
|
||||
File outputFile = new File(output);
|
||||
if (outputFile.exists() && !forceOverwrite) {
|
||||
System.err.println(output + " exists, use --force to overwrite");
|
||||
usage();
|
||||
return;
|
||||
}
|
||||
|
||||
String outputApkFileParentPath = outputFile.getParent();
|
||||
if (outputApkFileParentPath == null) {
|
||||
String absPath = outputFile.getAbsolutePath();
|
||||
int index = absPath.lastIndexOf(File.separatorChar);
|
||||
outputApkFileParentPath = absPath.substring(0, index);
|
||||
}
|
||||
|
||||
System.out.println(" !!!!! output apk path --> " + output +
|
||||
" disableCrackSignature --> " + disableCrackSignature);
|
||||
|
||||
String apkFileName = getBaseName(srcApkFile);
|
||||
|
||||
// 中间文件临时存储的位置
|
||||
String tempFilePath = outputApkFileParentPath + File.separator +
|
||||
currentTimeStr() + "-tmp" + File.separator;
|
||||
|
||||
// apk文件解压的目录
|
||||
unzipApkFilePath = tempFilePath + apkFileName + "-" + UNZIP_APK_FILE_NAME + File.separator;
|
||||
|
||||
if (showAllLogs) {
|
||||
System.out.println(" !!!!! outputApkFileParentPath = " + outputApkFileParentPath +
|
||||
"\n unzipApkFilePath = " + unzipApkFilePath);
|
||||
}
|
||||
|
||||
if (!disableCrackSignature) {
|
||||
// save the apk original signature info, to support crach signature.
|
||||
new SaveApkSignatureTask(apkPath, unzipApkFilePath).run();
|
||||
}
|
||||
|
||||
// 先解压apk到指定目录下
|
||||
long currentTime = System.currentTimeMillis();
|
||||
FileUtils.decompressZip(apkPath, unzipApkFilePath);
|
||||
|
||||
if (showAllLogs) {
|
||||
System.out.println(" decompress apk cost time: " + (System.currentTimeMillis() - currentTime));
|
||||
}
|
||||
|
||||
// Get the dex count in the apk zip file
|
||||
dexFileCount = findDexFileCount(unzipApkFilePath);
|
||||
|
||||
if (showAllLogs) {
|
||||
System.out.println(" --- dexFileCount = " + dexFileCount);
|
||||
}
|
||||
|
||||
String manifestFilePath = unzipApkFilePath + "AndroidManifest.xml";
|
||||
|
||||
currentTime = System.currentTimeMillis();
|
||||
|
||||
// parse the app main application full name from the manifest file
|
||||
ManifestParser.Pair pair = ManifestParser.parseManifestFile(manifestFilePath);
|
||||
String applicationName = null;
|
||||
if (pair != null && pair.applicationName != null) {
|
||||
applicationName = pair.applicationName;
|
||||
}
|
||||
|
||||
if (showAllLogs) {
|
||||
System.out.println(" Get application name cost time: " + (System.currentTimeMillis() - currentTime));
|
||||
System.out.println(" Get the application name --> " + applicationName);
|
||||
}
|
||||
|
||||
// modify manifest
|
||||
File manifestFile = new File(manifestFilePath);
|
||||
String manifestFilePathNew = unzipApkFilePath + "AndroidManifest" + "-" + currentTimeStr() + ".xml";
|
||||
File manifestFileNew = new File(manifestFilePathNew);
|
||||
manifestFile.renameTo(manifestFileNew);
|
||||
|
||||
modifyManifestFile(manifestFilePathNew, manifestFilePath, applicationName);
|
||||
|
||||
// new manifest may not exist
|
||||
if (manifestFile.exists() && manifestFile.length() > 0) {
|
||||
manifestFileNew.delete();
|
||||
} else {
|
||||
manifestFileNew.renameTo(manifestFile);
|
||||
}
|
||||
|
||||
// save original main application name to asset file
|
||||
if (isNotEmpty(applicationName)) {
|
||||
mXpatchTasks.add(new SaveOriginalApplicationNameTask(applicationName, unzipApkFilePath));
|
||||
}
|
||||
|
||||
// modify the apk dex file to make xposed can run in it
|
||||
if (dexModificationMode && isNotEmpty(applicationName)) {
|
||||
mXpatchTasks.add(new ApkModifyTask(showAllLogs, keepBuildFiles, unzipApkFilePath, applicationName,
|
||||
dexFileCount));
|
||||
}
|
||||
|
||||
// copy xposed so and dex files into the unzipped apk
|
||||
mXpatchTasks.add(new SoAndDexCopyTask(dexFileCount, unzipApkFilePath,
|
||||
getXposedModules(xposedModules), useWhaleHookFramework));
|
||||
|
||||
// compress all files into an apk and then sign it.
|
||||
mXpatchTasks.add(new BuildAndSignApkTask(keepBuildFiles, unzipApkFilePath, output));
|
||||
|
||||
// excute these tasks
|
||||
for (Runnable executor : mXpatchTasks) {
|
||||
currentTime = System.currentTimeMillis();
|
||||
executor.run();
|
||||
|
||||
if (showAllLogs) {
|
||||
System.out.println(executor.getClass().getSimpleName() + " cost time: "
|
||||
+ (System.currentTimeMillis() - currentTime));
|
||||
}
|
||||
}
|
||||
|
||||
// 5. delete all the build files that is useless now
|
||||
File unzipApkFile = new File(unzipApkFilePath);
|
||||
if (!keepBuildFiles && unzipApkFile.exists()) {
|
||||
FileUtils.deleteDir(unzipApkFile);
|
||||
}
|
||||
|
||||
File tempFile = new File(tempFilePath);
|
||||
if (!keepBuildFiles && tempFile.exists()) {
|
||||
tempFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
private void modifyManifestFile(String filePath, String dstFilePath, String originalApplicationName) {
|
||||
ModificationProperty property = new ModificationProperty();
|
||||
boolean modifyEnabled = false;
|
||||
if (isNotEmpty(newPackageName)) {
|
||||
modifyEnabled = true;
|
||||
property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.PACKAGE, newPackageName).setNamespace(null));
|
||||
}
|
||||
|
||||
if (versionCode > 0) {
|
||||
modifyEnabled = true;
|
||||
property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.VERSION_CODE, versionCode));
|
||||
}
|
||||
|
||||
if (isNotEmpty(versionName)) {
|
||||
modifyEnabled = true;
|
||||
property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.VERSION_NAME, versionName));
|
||||
}
|
||||
|
||||
if (debuggable >= 0) {
|
||||
modifyEnabled = true;
|
||||
property.addApplicationAttribute(new AttributeItem(NodeValue.Application.DEBUGGABLE, debuggable != 0));
|
||||
}
|
||||
|
||||
if (!dexModificationMode || !isNotEmpty(originalApplicationName)) {
|
||||
modifyEnabled = true;
|
||||
property.addApplicationAttribute(new AttributeItem(NodeValue.Application.NAME, PROXY_APPLICATION_NAME));
|
||||
}
|
||||
|
||||
if (modifyEnabled) {
|
||||
FileProcesser.processManifestFile(filePath, dstFilePath, property);
|
||||
}
|
||||
}
|
||||
|
||||
private int findDexFileCount(String unzipApkFilePath) {
|
||||
File zipfileRoot = new File(unzipApkFilePath);
|
||||
if (!zipfileRoot.exists()) {
|
||||
return 0;
|
||||
}
|
||||
File[] childFiles = zipfileRoot.listFiles();
|
||||
if (childFiles == null || childFiles.length == 0) {
|
||||
return 0;
|
||||
}
|
||||
int count = 0;
|
||||
for (File file : childFiles) {
|
||||
String fileName = file.getName();
|
||||
if (Pattern.matches("classes.*\\.dex", fileName)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// Use the current timestamp as the name of the build file
|
||||
private String currentTimeStr() {
|
||||
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");//设置日期格式
|
||||
return df.format(new Date());
|
||||
}
|
||||
|
||||
private String[] getXposedModules(String modules) {
|
||||
if (modules == null || modules.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return modules.split(File.pathSeparator);
|
||||
}
|
||||
|
||||
private static boolean isNotEmpty(String str) {
|
||||
return str != null && !str.isEmpty();
|
||||
}
|
||||
}
|
||||
package com.storm.wind.xpatch;
|
||||
|
||||
import com.storm.wind.xpatch.base.BaseCommand;
|
||||
import com.storm.wind.xpatch.task.ApkModifyTask;
|
||||
import com.storm.wind.xpatch.task.BuildAndSignApkTask;
|
||||
import com.storm.wind.xpatch.task.SaveApkSignatureTask;
|
||||
import com.storm.wind.xpatch.task.SaveOriginalApplicationNameTask;
|
||||
import com.storm.wind.xpatch.task.SoAndDexCopyTask;
|
||||
import com.storm.wind.xpatch.util.FileUtils;
|
||||
import com.storm.wind.xpatch.util.ManifestParser;
|
||||
import com.wind.meditor.core.FileProcesser;
|
||||
import com.wind.meditor.property.AttributeItem;
|
||||
import com.wind.meditor.property.ModificationProperty;
|
||||
import com.wind.meditor.utils.NodeValue;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class MainCommand extends BaseCommand {
|
||||
|
||||
private String apkPath;
|
||||
|
||||
private String unzipApkFilePath;
|
||||
|
||||
@Opt(opt = "o", longOpt = "output", description = "output .apk file, default is " +
|
||||
"$source_apk_dir/[file-name]-xposed-signed.apk", argName = "out-apk-file")
|
||||
private String output; // 输出的apk文件的目录以及名称
|
||||
|
||||
@Opt(opt = "f", longOpt = "force", hasArg = false, description = "force overwrite")
|
||||
private boolean forceOverwrite = false;
|
||||
|
||||
@Opt(opt = "k", longOpt = "keep", hasArg = false, description = "not delete the jar file " +
|
||||
"that is changed by dex2jar and the apk zip files")
|
||||
private boolean keepBuildFiles = false;
|
||||
|
||||
@Opt(opt = "l", longOpt = "log", hasArg = false, description = "show all the debug logs")
|
||||
private boolean showAllLogs = false;
|
||||
|
||||
@Opt(opt = "c", longOpt = "crach", hasArg = false,
|
||||
description = "disable craching the apk's signature.")
|
||||
private boolean disableCrackSignature = false;
|
||||
|
||||
@Opt(opt = "xm", longOpt = "xposed-modules", description = "the xposed module files to be packaged into the apk, " +
|
||||
"multi files should be seperated by :(mac) or ;(win) ", argName = "xposed module file path")
|
||||
private String xposedModules;
|
||||
|
||||
// 使用dex文件中插入代码的方式修改apk,而不是默认的修改Manifest中Application name的方式
|
||||
@Opt(opt = "dex", longOpt = "dex", hasArg = false, description = "insert code into the dex file, not modify manifest application name attribute")
|
||||
private boolean dexModificationMode = false;
|
||||
|
||||
@Opt(opt = "pkg", longOpt = "packageName", description = "modify the apk package name", argName = "new package name")
|
||||
private String newPackageName;
|
||||
|
||||
@Opt(opt = "d", longOpt = "debuggable", description = "set 1 to make the app debuggable = true, " +
|
||||
"set 0 to make the app debuggable = false", argName = "0 or 1")
|
||||
private int debuggable = -1; // 0: debuggable = false 1: debuggable = true
|
||||
|
||||
@Opt(opt = "vc", longOpt = "versionCode", description = "set the app version code",
|
||||
argName = "new-version-code")
|
||||
private int versionCode;
|
||||
|
||||
@Opt(opt = "vn", longOpt = "versionName", description = "set the app version name",
|
||||
argName = "new-version-name")
|
||||
private String versionName;
|
||||
|
||||
@Opt(opt = "w", longOpt = "whale", hasArg = false, description = "Change hook framework to Lody's whale")
|
||||
private boolean useWhaleHookFramework = false; // 是否使用whale hook框架,默认使用的是SandHook
|
||||
|
||||
// 原来apk中dex文件的数量
|
||||
private int dexFileCount = 0;
|
||||
|
||||
private static final String UNZIP_APK_FILE_NAME = "apk-unzip-files";
|
||||
|
||||
private static final String PROXY_APPLICATION_NAME = "com.wind.xpatch.proxy.XpatchProxyApplication";
|
||||
|
||||
private List<Runnable> mXpatchTasks = new ArrayList<>();
|
||||
|
||||
public static void main(String... args) {
|
||||
new MainCommand().doMain(args);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doCommandLine() {
|
||||
if (remainingArgs.length != 1) {
|
||||
if (remainingArgs.length == 0) {
|
||||
System.out.println("Please choose one apk file you want to process. ");
|
||||
}
|
||||
if (remainingArgs.length > 1) {
|
||||
System.out.println("This tool can only used with one apk file.");
|
||||
}
|
||||
usage();
|
||||
return;
|
||||
}
|
||||
|
||||
apkPath = remainingArgs[0];
|
||||
|
||||
File srcApkFile = new File(apkPath);
|
||||
|
||||
if (!srcApkFile.exists()) {
|
||||
System.out.println(" The source apk file not exsit, please choose another one. " +
|
||||
"current apk file is = " + apkPath);
|
||||
return;
|
||||
}
|
||||
|
||||
String currentDir = new File(".").getAbsolutePath(); // 当前命令行所在的目录
|
||||
if (showAllLogs) {
|
||||
System.out.println(" currentDir = " + currentDir + " \n apkPath = " + apkPath);
|
||||
}
|
||||
|
||||
if (output == null || output.length() == 0) {
|
||||
output = getBaseName(apkPath) + "-xposed-signed.apk";
|
||||
}
|
||||
|
||||
File outputFile = new File(output);
|
||||
if (outputFile.exists() && !forceOverwrite) {
|
||||
System.err.println(output + " exists, use --force to overwrite");
|
||||
usage();
|
||||
return;
|
||||
}
|
||||
|
||||
String outputApkFileParentPath = outputFile.getParent();
|
||||
if (outputApkFileParentPath == null) {
|
||||
String absPath = outputFile.getAbsolutePath();
|
||||
int index = absPath.lastIndexOf(File.separatorChar);
|
||||
outputApkFileParentPath = absPath.substring(0, index);
|
||||
}
|
||||
|
||||
System.out.println(" !!!!! output apk path --> " + output +
|
||||
" disableCrackSignature --> " + disableCrackSignature);
|
||||
|
||||
String apkFileName = getBaseName(srcApkFile);
|
||||
|
||||
// 中间文件临时存储的位置
|
||||
String tempFilePath = outputApkFileParentPath + File.separator +
|
||||
currentTimeStr() + "-tmp" + File.separator;
|
||||
|
||||
// apk文件解压的目录
|
||||
unzipApkFilePath = tempFilePath + apkFileName + "-" + UNZIP_APK_FILE_NAME + File.separator;
|
||||
|
||||
if (showAllLogs) {
|
||||
System.out.println(" !!!!! outputApkFileParentPath = " + outputApkFileParentPath +
|
||||
"\n unzipApkFilePath = " + unzipApkFilePath);
|
||||
}
|
||||
|
||||
if (!disableCrackSignature) {
|
||||
// save the apk original signature info, to support crach signature.
|
||||
new SaveApkSignatureTask(apkPath, unzipApkFilePath).run();
|
||||
}
|
||||
|
||||
// 先解压apk到指定目录下
|
||||
long currentTime = System.currentTimeMillis();
|
||||
FileUtils.decompressZip(apkPath, unzipApkFilePath);
|
||||
|
||||
if (showAllLogs) {
|
||||
System.out.println(" decompress apk cost time: " + (System.currentTimeMillis() - currentTime));
|
||||
}
|
||||
|
||||
// Get the dex count in the apk zip file
|
||||
dexFileCount = findDexFileCount(unzipApkFilePath);
|
||||
|
||||
if (showAllLogs) {
|
||||
System.out.println(" --- dexFileCount = " + dexFileCount);
|
||||
}
|
||||
|
||||
String manifestFilePath = unzipApkFilePath + "AndroidManifest.xml";
|
||||
|
||||
currentTime = System.currentTimeMillis();
|
||||
|
||||
// parse the app main application full name from the manifest file
|
||||
ManifestParser.Pair pair = ManifestParser.parseManifestFile(manifestFilePath);
|
||||
String applicationName = null;
|
||||
if (pair != null && pair.applicationName != null) {
|
||||
applicationName = pair.applicationName;
|
||||
}
|
||||
|
||||
if (showAllLogs) {
|
||||
System.out.println(" Get application name cost time: " + (System.currentTimeMillis() - currentTime));
|
||||
System.out.println(" Get the application name --> " + applicationName);
|
||||
}
|
||||
|
||||
// modify manifest
|
||||
File manifestFile = new File(manifestFilePath);
|
||||
String manifestFilePathNew = unzipApkFilePath + "AndroidManifest" + "-" + currentTimeStr() + ".xml";
|
||||
File manifestFileNew = new File(manifestFilePathNew);
|
||||
manifestFile.renameTo(manifestFileNew);
|
||||
|
||||
modifyManifestFile(manifestFilePathNew, manifestFilePath, applicationName);
|
||||
|
||||
// new manifest may not exist
|
||||
if (manifestFile.exists() && manifestFile.length() > 0) {
|
||||
manifestFileNew.delete();
|
||||
} else {
|
||||
manifestFileNew.renameTo(manifestFile);
|
||||
}
|
||||
|
||||
// save original main application name to asset file
|
||||
if (isNotEmpty(applicationName)) {
|
||||
mXpatchTasks.add(new SaveOriginalApplicationNameTask(applicationName, unzipApkFilePath));
|
||||
}
|
||||
|
||||
// modify the apk dex file to make xposed can run in it
|
||||
if (dexModificationMode && isNotEmpty(applicationName)) {
|
||||
mXpatchTasks.add(new ApkModifyTask(showAllLogs, keepBuildFiles, unzipApkFilePath, applicationName,
|
||||
dexFileCount));
|
||||
}
|
||||
|
||||
// copy xposed so and dex files into the unzipped apk
|
||||
mXpatchTasks.add(new SoAndDexCopyTask(dexFileCount, unzipApkFilePath,
|
||||
getXposedModules(xposedModules), useWhaleHookFramework));
|
||||
|
||||
// compress all files into an apk and then sign it.
|
||||
mXpatchTasks.add(new BuildAndSignApkTask(keepBuildFiles, unzipApkFilePath, output));
|
||||
|
||||
// copy origin apk to assets
|
||||
// convenient to bypass some check like CRC
|
||||
if(!FileUtils.copyFile(srcApkFile, new File(unzipApkFilePath, "assets/origin_apk.bin"))){
|
||||
throw new IllegalStateException("orignal apk copy fail");
|
||||
}
|
||||
|
||||
// excute these tasks
|
||||
for (Runnable executor : mXpatchTasks) {
|
||||
currentTime = System.currentTimeMillis();
|
||||
executor.run();
|
||||
|
||||
if (showAllLogs) {
|
||||
System.out.println(executor.getClass().getSimpleName() + " cost time: "
|
||||
+ (System.currentTimeMillis() - currentTime));
|
||||
}
|
||||
}
|
||||
|
||||
// 5. delete all the build files that is useless now
|
||||
File unzipApkFile = new File(unzipApkFilePath);
|
||||
if (!keepBuildFiles && unzipApkFile.exists()) {
|
||||
FileUtils.deleteDir(unzipApkFile);
|
||||
}
|
||||
|
||||
File tempFile = new File(tempFilePath);
|
||||
if (!keepBuildFiles && tempFile.exists()) {
|
||||
tempFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
private void modifyManifestFile(String filePath, String dstFilePath, String originalApplicationName) {
|
||||
ModificationProperty property = new ModificationProperty();
|
||||
boolean modifyEnabled = false;
|
||||
if (isNotEmpty(newPackageName)) {
|
||||
modifyEnabled = true;
|
||||
property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.PACKAGE, newPackageName).setNamespace(null));
|
||||
}
|
||||
|
||||
if (versionCode > 0) {
|
||||
modifyEnabled = true;
|
||||
property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.VERSION_CODE, versionCode));
|
||||
}
|
||||
|
||||
if (isNotEmpty(versionName)) {
|
||||
modifyEnabled = true;
|
||||
property.addManifestAttribute(new AttributeItem(NodeValue.Manifest.VERSION_NAME, versionName));
|
||||
}
|
||||
|
||||
if (debuggable >= 0) {
|
||||
modifyEnabled = true;
|
||||
property.addApplicationAttribute(new AttributeItem(NodeValue.Application.DEBUGGABLE, debuggable != 0));
|
||||
}
|
||||
|
||||
if (!dexModificationMode || !isNotEmpty(originalApplicationName)) {
|
||||
modifyEnabled = true;
|
||||
property.addApplicationAttribute(new AttributeItem(NodeValue.Application.NAME, PROXY_APPLICATION_NAME));
|
||||
}
|
||||
|
||||
if (modifyEnabled) {
|
||||
FileProcesser.processManifestFile(filePath, dstFilePath, property);
|
||||
}
|
||||
}
|
||||
|
||||
private int findDexFileCount(String unzipApkFilePath) {
|
||||
File zipfileRoot = new File(unzipApkFilePath);
|
||||
if (!zipfileRoot.exists()) {
|
||||
return 0;
|
||||
}
|
||||
File[] childFiles = zipfileRoot.listFiles();
|
||||
if (childFiles == null || childFiles.length == 0) {
|
||||
return 0;
|
||||
}
|
||||
int count = 0;
|
||||
for (File file : childFiles) {
|
||||
String fileName = file.getName();
|
||||
if (Pattern.matches("classes.*\\.dex", fileName)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// Use the current timestamp as the name of the build file
|
||||
private String currentTimeStr() {
|
||||
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");//设置日期格式
|
||||
return df.format(new Date());
|
||||
}
|
||||
|
||||
private String[] getXposedModules(String modules) {
|
||||
if (modules == null || modules.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return modules.split(File.pathSeparator);
|
||||
}
|
||||
|
||||
private static boolean isNotEmpty(String str) {
|
||||
return str != null && !str.isEmpty();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,478 +1,478 @@
|
|||
package com.storm.wind.xpatch.base;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* Created by Wind
|
||||
*/
|
||||
public abstract class BaseCommand {
|
||||
|
||||
private String onlineHelp;
|
||||
|
||||
protected Map<String, Option> optMap = new HashMap<String, Option>();
|
||||
|
||||
@Opt(opt = "h", longOpt = "help", hasArg = false, description = "Print this help message")
|
||||
private boolean printHelp = false;
|
||||
|
||||
protected String remainingArgs[];
|
||||
protected String orginalArgs[];
|
||||
|
||||
@Retention(value = RetentionPolicy.RUNTIME)
|
||||
@Target(value = { ElementType.FIELD })
|
||||
static public @interface Opt {
|
||||
String argName() default "";
|
||||
|
||||
String description() default "";
|
||||
|
||||
boolean hasArg() default true;
|
||||
|
||||
String longOpt() default "";
|
||||
|
||||
String opt() default "";
|
||||
|
||||
boolean required() default false;
|
||||
}
|
||||
|
||||
static protected class Option implements Comparable<Option> {
|
||||
public String argName = "arg";
|
||||
public String description;
|
||||
public Field field;
|
||||
public boolean hasArg = true;
|
||||
public String longOpt;
|
||||
public String opt;
|
||||
public boolean required = false;
|
||||
|
||||
@Override
|
||||
public int compareTo(Option o) {
|
||||
int result = s(this.opt, o.opt);
|
||||
if (result == 0) {
|
||||
result = s(this.longOpt, o.longOpt);
|
||||
if (result == 0) {
|
||||
result = s(this.argName, o.argName);
|
||||
if (result == 0) {
|
||||
result = s(this.description, o.description);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int s(String a, String b) {
|
||||
if (a != null && b != null) {
|
||||
return a.compareTo(b);
|
||||
} else if (a != null) {
|
||||
return 1;
|
||||
} else if (b != null) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public String getOptAndLongOpt() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
boolean havePrev = false;
|
||||
if (opt != null && opt.length() > 0) {
|
||||
sb.append("-").append(opt);
|
||||
havePrev = true;
|
||||
}
|
||||
if (longOpt != null && longOpt.length() > 0) {
|
||||
if (havePrev) {
|
||||
sb.append(",");
|
||||
}
|
||||
sb.append("--").append(longOpt);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Retention(value = RetentionPolicy.RUNTIME)
|
||||
@Target(value = { ElementType.TYPE })
|
||||
static public @interface Syntax {
|
||||
|
||||
String cmd();
|
||||
|
||||
String desc() default "";
|
||||
|
||||
String onlineHelp() default "";
|
||||
|
||||
String syntax() default "";
|
||||
}
|
||||
|
||||
public void doMain(String... args) {
|
||||
try {
|
||||
initOptions();
|
||||
parseSetArgs(args);
|
||||
doCommandLine();
|
||||
} catch (HelpException e) {
|
||||
String msg = e.getMessage();
|
||||
if (msg != null && msg.length() > 0) {
|
||||
System.err.println("ERROR: " + msg);
|
||||
}
|
||||
usage();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void doCommandLine() throws Exception;
|
||||
|
||||
protected String getVersionString() {
|
||||
return getClass().getPackage().getImplementationVersion();
|
||||
}
|
||||
|
||||
protected void initOptions() {
|
||||
initOptionFromClass(this.getClass());
|
||||
}
|
||||
|
||||
protected void initOptionFromClass(Class<?> clz) {
|
||||
if (clz == null) {
|
||||
return;
|
||||
} else {
|
||||
initOptionFromClass(clz.getSuperclass());
|
||||
}
|
||||
|
||||
Syntax syntax = clz.getAnnotation(Syntax.class);
|
||||
if (syntax != null) {
|
||||
this.onlineHelp = syntax.onlineHelp();
|
||||
}
|
||||
|
||||
Field[] fs = clz.getDeclaredFields();
|
||||
for (Field f : fs) {
|
||||
Opt opt = f.getAnnotation(Opt.class);
|
||||
if (opt != null) {
|
||||
f.setAccessible(true);
|
||||
Option option = new Option();
|
||||
option.field = f;
|
||||
option.description = opt.description();
|
||||
option.hasArg = opt.hasArg();
|
||||
option.required = opt.required();
|
||||
if ("".equals(opt.longOpt()) && "".equals(opt.opt())) { // into automode
|
||||
option.longOpt = fromCamel(f.getName());
|
||||
if (f.getType().equals(boolean.class)) {
|
||||
option.hasArg=false;
|
||||
try {
|
||||
if (f.getBoolean(this)) {
|
||||
throw new RuntimeException("the value of " + f +
|
||||
" must be false, as it is declared as no args");
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
checkConflict(option, "--" + option.longOpt);
|
||||
continue;
|
||||
}
|
||||
if (!opt.hasArg()) {
|
||||
if (!f.getType().equals(boolean.class)) {
|
||||
throw new RuntimeException("the type of " + f
|
||||
+ " must be boolean, as it is declared as no args");
|
||||
}
|
||||
|
||||
try {
|
||||
if (f.getBoolean(this)) {
|
||||
throw new RuntimeException("the value of " + f +
|
||||
" must be false, as it is declared as no args");
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
boolean haveLongOpt = false;
|
||||
if (!"".equals(opt.longOpt())) {
|
||||
option.longOpt = opt.longOpt();
|
||||
checkConflict(option, "--" + option.longOpt);
|
||||
haveLongOpt = true;
|
||||
}
|
||||
if (!"".equals(opt.argName())) {
|
||||
option.argName = opt.argName();
|
||||
}
|
||||
if (!"".equals(opt.opt())) {
|
||||
option.opt = opt.opt();
|
||||
checkConflict(option, "-" + option.opt);
|
||||
} else {
|
||||
if (!haveLongOpt) {
|
||||
throw new RuntimeException("opt or longOpt is not set in @Opt(...) " + f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkConflict(Option option, String key) {
|
||||
if (optMap.containsKey(key)) {
|
||||
Option preOption = optMap.get(key);
|
||||
throw new RuntimeException(String.format("[@Opt(...) %s] conflict with [@Opt(...) %s]",
|
||||
preOption.field.toString(), option.field
|
||||
));
|
||||
}
|
||||
optMap.put(key, option);
|
||||
}
|
||||
|
||||
private static String fromCamel(String name) {
|
||||
if (name.length() == 0) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
char[] charArray = name.toCharArray();
|
||||
sb.append(Character.toLowerCase(charArray[0]));
|
||||
for (int i = 1; i < charArray.length; i++) {
|
||||
char c = charArray[i];
|
||||
if (Character.isUpperCase(c)) {
|
||||
sb.append("-").append(Character.toLowerCase(c));
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
protected void parseSetArgs(String... args) throws IllegalArgumentException, IllegalAccessException {
|
||||
this.orginalArgs = args;
|
||||
List<String> remainsOptions = new ArrayList<String>();
|
||||
Set<Option> requiredOpts = collectRequriedOptions(optMap);
|
||||
Option needArgOpt = null;
|
||||
for (String s : args) {
|
||||
if (needArgOpt != null) {
|
||||
Field field = needArgOpt.field;
|
||||
Class clazz = field.getType();
|
||||
if (clazz.equals(List.class)) {
|
||||
try {
|
||||
List<Object> object = ((List<Object>) field.get(this));
|
||||
|
||||
// 获取List对象的泛型类型
|
||||
ParameterizedType listGenericType = (ParameterizedType) field.getGenericType();
|
||||
Type[] listActualTypeArguments = listGenericType.getActualTypeArguments();
|
||||
Class typeClazz = (Class) listActualTypeArguments[0];
|
||||
object.add(convert(s, typeClazz));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
field.set(this, convert(s, clazz));
|
||||
}
|
||||
needArgOpt = null;
|
||||
} else if (s.startsWith("-")) {// its a short or long option
|
||||
Option opt = optMap.get(s);
|
||||
requiredOpts.remove(opt);
|
||||
if (opt == null) {
|
||||
System.err.println("ERROR: Unrecognized option: " + s);
|
||||
throw new HelpException();
|
||||
} else {
|
||||
if (opt.hasArg) {
|
||||
needArgOpt = opt;
|
||||
} else {
|
||||
opt.field.set(this, true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
remainsOptions.add(s);
|
||||
}
|
||||
}
|
||||
|
||||
if (needArgOpt != null) {
|
||||
System.err.println("ERROR: Option " + needArgOpt.getOptAndLongOpt() + " need an argument value");
|
||||
throw new HelpException();
|
||||
}
|
||||
this.remainingArgs = remainsOptions.toArray(new String[remainsOptions.size()]);
|
||||
if (this.printHelp) {
|
||||
throw new HelpException();
|
||||
}
|
||||
if (!requiredOpts.isEmpty()) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("ERROR: Options: ");
|
||||
boolean first = true;
|
||||
for (Option option : requiredOpts) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
sb.append(" and ");
|
||||
}
|
||||
sb.append(option.getOptAndLongOpt());
|
||||
}
|
||||
sb.append(" is required");
|
||||
System.err.println(sb.toString());
|
||||
throw new HelpException();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
protected Object convert(String value, Class type) {
|
||||
if (type.equals(String.class)) {
|
||||
return value;
|
||||
}
|
||||
if (type.equals(int.class) || type.equals(Integer.class)) {
|
||||
return Integer.parseInt(value);
|
||||
}
|
||||
if (type.equals(long.class) || type.equals(Long.class)) {
|
||||
return Long.parseLong(value);
|
||||
}
|
||||
if (type.equals(float.class) || type.equals(Float.class)) {
|
||||
return Float.parseFloat(value);
|
||||
}
|
||||
if (type.equals(double.class) || type.equals(Double.class)) {
|
||||
return Double.parseDouble(value);
|
||||
}
|
||||
if (type.equals(boolean.class) || type.equals(Boolean.class)) {
|
||||
return Boolean.parseBoolean(value);
|
||||
}
|
||||
if (type.equals(File.class)) {
|
||||
return new File(value);
|
||||
}
|
||||
if (type.equals(Path.class)) {
|
||||
return new File(value).toPath();
|
||||
}
|
||||
try {
|
||||
type.asSubclass(Enum.class);
|
||||
return Enum.valueOf(type, value);
|
||||
} catch (Exception e) {
|
||||
}
|
||||
|
||||
throw new RuntimeException("can't convert [" + value + "] to type " + type);
|
||||
}
|
||||
|
||||
private Set<Option> collectRequriedOptions(Map<String, Option> optMap) {
|
||||
Set<Option> options = new HashSet<Option>();
|
||||
for (Map.Entry<String, Option> e : optMap.entrySet()) {
|
||||
Option option = e.getValue();
|
||||
if (option.required) {
|
||||
options.add(option);
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
protected static class HelpException extends RuntimeException {
|
||||
|
||||
public HelpException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public HelpException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected void usage() {
|
||||
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.err, StandardCharsets.UTF_8), true);
|
||||
|
||||
final int maxLength = 80;
|
||||
final int maxPaLength = 40;
|
||||
// out.println(this.cmdName + " -- " + desc);
|
||||
// out.println("usage: " + this.cmdName + " " + cmdLineSyntax);
|
||||
if (this.optMap.size() > 0) {
|
||||
out.println("options:");
|
||||
}
|
||||
// [PART.A.........][Part.B
|
||||
// .-a,--aa.<arg>...desc1
|
||||
// .................desc2
|
||||
// .-b,--bb
|
||||
TreeSet<Option> options = new TreeSet<Option>(this.optMap.values());
|
||||
int palength = -1;
|
||||
for (Option option : options) {
|
||||
int pa = 4 + option.getOptAndLongOpt().length();
|
||||
if (option.hasArg) {
|
||||
pa += 3 + option.argName.length();
|
||||
}
|
||||
if (pa < maxPaLength) {
|
||||
if (pa > palength) {
|
||||
palength = pa;
|
||||
}
|
||||
}
|
||||
}
|
||||
int pblength = maxLength - palength;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Option option : options) {
|
||||
sb.setLength(0);
|
||||
sb.append(" ").append(option.getOptAndLongOpt());
|
||||
if (option.hasArg) {
|
||||
sb.append(" <").append(option.argName).append(">");
|
||||
}
|
||||
String desc = option.description;
|
||||
if (desc == null || desc.length() == 0) {// no description
|
||||
out.println(sb);
|
||||
} else {
|
||||
for (int i = palength - sb.length(); i > 0; i--) {
|
||||
sb.append(' ');
|
||||
}
|
||||
if (sb.length() > maxPaLength) {// to huge part A
|
||||
out.println(sb);
|
||||
sb.setLength(0);
|
||||
for (int i = 0; i < palength; i++) {
|
||||
sb.append(' ');
|
||||
}
|
||||
}
|
||||
int nextStart = 0;
|
||||
while (nextStart < desc.length()) {
|
||||
if (desc.length() - nextStart < pblength) {// can put in one line
|
||||
sb.append(desc.substring(nextStart));
|
||||
out.println(sb);
|
||||
nextStart = desc.length();
|
||||
sb.setLength(0);
|
||||
} else {
|
||||
sb.append(desc.substring(nextStart, nextStart + pblength));
|
||||
out.println(sb);
|
||||
nextStart += pblength;
|
||||
sb.setLength(0);
|
||||
if (nextStart < desc.length()) {
|
||||
for (int i = 0; i < palength; i++) {
|
||||
sb.append(' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sb.length() > 0) {
|
||||
out.println(sb);
|
||||
sb.setLength(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
String ver = getVersionString();
|
||||
if (ver != null && !"".equals(ver)) {
|
||||
out.println("version: " + ver);
|
||||
}
|
||||
if (onlineHelp != null && !"".equals(onlineHelp)) {
|
||||
if (onlineHelp.length() + "online help: ".length() > maxLength) {
|
||||
out.println("online help: ");
|
||||
out.println(onlineHelp);
|
||||
} else {
|
||||
out.println("online help: " + onlineHelp);
|
||||
}
|
||||
}
|
||||
out.flush();
|
||||
}
|
||||
|
||||
public static String getBaseName(String fn) {
|
||||
int x = fn.lastIndexOf('.');
|
||||
return x >= 0 ? fn.substring(0, x) : fn;
|
||||
}
|
||||
|
||||
// 获取文件不包含后缀的名称
|
||||
public static String getBaseName(File fn) {
|
||||
return getBaseName(fn.getName());
|
||||
}
|
||||
|
||||
}
|
||||
package com.storm.wind.xpatch.base;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* Created by Wind
|
||||
*/
|
||||
public abstract class BaseCommand {
|
||||
|
||||
private String onlineHelp;
|
||||
|
||||
protected Map<String, Option> optMap = new HashMap<String, Option>();
|
||||
|
||||
@Opt(opt = "h", longOpt = "help", hasArg = false, description = "Print this help message")
|
||||
private boolean printHelp = false;
|
||||
|
||||
protected String[] remainingArgs;
|
||||
protected String[] orginalArgs;
|
||||
|
||||
@Retention(value = RetentionPolicy.RUNTIME)
|
||||
@Target(value = { ElementType.FIELD })
|
||||
static public @interface Opt {
|
||||
String argName() default "";
|
||||
|
||||
String description() default "";
|
||||
|
||||
boolean hasArg() default true;
|
||||
|
||||
String longOpt() default "";
|
||||
|
||||
String opt() default "";
|
||||
|
||||
boolean required() default false;
|
||||
}
|
||||
|
||||
static protected class Option implements Comparable<Option> {
|
||||
public String argName = "arg";
|
||||
public String description;
|
||||
public Field field;
|
||||
public boolean hasArg = true;
|
||||
public String longOpt;
|
||||
public String opt;
|
||||
public boolean required = false;
|
||||
|
||||
@Override
|
||||
public int compareTo(Option o) {
|
||||
int result = s(this.opt, o.opt);
|
||||
if (result == 0) {
|
||||
result = s(this.longOpt, o.longOpt);
|
||||
if (result == 0) {
|
||||
result = s(this.argName, o.argName);
|
||||
if (result == 0) {
|
||||
result = s(this.description, o.description);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int s(String a, String b) {
|
||||
if (a != null && b != null) {
|
||||
return a.compareTo(b);
|
||||
} else if (a != null) {
|
||||
return 1;
|
||||
} else if (b != null) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public String getOptAndLongOpt() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
boolean havePrev = false;
|
||||
if (opt != null && opt.length() > 0) {
|
||||
sb.append("-").append(opt);
|
||||
havePrev = true;
|
||||
}
|
||||
if (longOpt != null && longOpt.length() > 0) {
|
||||
if (havePrev) {
|
||||
sb.append(",");
|
||||
}
|
||||
sb.append("--").append(longOpt);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Retention(value = RetentionPolicy.RUNTIME)
|
||||
@Target(value = { ElementType.TYPE })
|
||||
static public @interface Syntax {
|
||||
|
||||
String cmd();
|
||||
|
||||
String desc() default "";
|
||||
|
||||
String onlineHelp() default "";
|
||||
|
||||
String syntax() default "";
|
||||
}
|
||||
|
||||
public void doMain(String... args) {
|
||||
try {
|
||||
initOptions();
|
||||
parseSetArgs(args);
|
||||
doCommandLine();
|
||||
} catch (HelpException e) {
|
||||
String msg = e.getMessage();
|
||||
if (msg != null && msg.length() > 0) {
|
||||
System.err.println("ERROR: " + msg);
|
||||
}
|
||||
usage();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void doCommandLine() throws Exception;
|
||||
|
||||
protected String getVersionString() {
|
||||
return getClass().getPackage().getImplementationVersion();
|
||||
}
|
||||
|
||||
protected void initOptions() {
|
||||
initOptionFromClass(this.getClass());
|
||||
}
|
||||
|
||||
protected void initOptionFromClass(Class<?> clz) {
|
||||
if (clz == null) {
|
||||
return;
|
||||
} else {
|
||||
initOptionFromClass(clz.getSuperclass());
|
||||
}
|
||||
|
||||
Syntax syntax = clz.getAnnotation(Syntax.class);
|
||||
if (syntax != null) {
|
||||
this.onlineHelp = syntax.onlineHelp();
|
||||
}
|
||||
|
||||
Field[] fs = clz.getDeclaredFields();
|
||||
for (Field f : fs) {
|
||||
Opt opt = f.getAnnotation(Opt.class);
|
||||
if (opt != null) {
|
||||
f.setAccessible(true);
|
||||
Option option = new Option();
|
||||
option.field = f;
|
||||
option.description = opt.description();
|
||||
option.hasArg = opt.hasArg();
|
||||
option.required = opt.required();
|
||||
if ("".equals(opt.longOpt()) && "".equals(opt.opt())) { // into automode
|
||||
option.longOpt = fromCamel(f.getName());
|
||||
if (f.getType().equals(boolean.class)) {
|
||||
option.hasArg=false;
|
||||
try {
|
||||
if (f.getBoolean(this)) {
|
||||
throw new RuntimeException("the value of " + f +
|
||||
" must be false, as it is declared as no args");
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
checkConflict(option, "--" + option.longOpt);
|
||||
continue;
|
||||
}
|
||||
if (!opt.hasArg()) {
|
||||
if (!f.getType().equals(boolean.class)) {
|
||||
throw new RuntimeException("the type of " + f
|
||||
+ " must be boolean, as it is declared as no args");
|
||||
}
|
||||
|
||||
try {
|
||||
if (f.getBoolean(this)) {
|
||||
throw new RuntimeException("the value of " + f +
|
||||
" must be false, as it is declared as no args");
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
boolean haveLongOpt = false;
|
||||
if (!"".equals(opt.longOpt())) {
|
||||
option.longOpt = opt.longOpt();
|
||||
checkConflict(option, "--" + option.longOpt);
|
||||
haveLongOpt = true;
|
||||
}
|
||||
if (!"".equals(opt.argName())) {
|
||||
option.argName = opt.argName();
|
||||
}
|
||||
if (!"".equals(opt.opt())) {
|
||||
option.opt = opt.opt();
|
||||
checkConflict(option, "-" + option.opt);
|
||||
} else {
|
||||
if (!haveLongOpt) {
|
||||
throw new RuntimeException("opt or longOpt is not set in @Opt(...) " + f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkConflict(Option option, String key) {
|
||||
if (optMap.containsKey(key)) {
|
||||
Option preOption = optMap.get(key);
|
||||
throw new RuntimeException(String.format("[@Opt(...) %s] conflict with [@Opt(...) %s]",
|
||||
preOption.field.toString(), option.field
|
||||
));
|
||||
}
|
||||
optMap.put(key, option);
|
||||
}
|
||||
|
||||
private static String fromCamel(String name) {
|
||||
if (name.length() == 0) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
char[] charArray = name.toCharArray();
|
||||
sb.append(Character.toLowerCase(charArray[0]));
|
||||
for (int i = 1; i < charArray.length; i++) {
|
||||
char c = charArray[i];
|
||||
if (Character.isUpperCase(c)) {
|
||||
sb.append("-").append(Character.toLowerCase(c));
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
protected void parseSetArgs(String... args) throws IllegalArgumentException, IllegalAccessException {
|
||||
this.orginalArgs = args;
|
||||
List<String> remainsOptions = new ArrayList<String>();
|
||||
Set<Option> requiredOpts = collectRequriedOptions(optMap);
|
||||
Option needArgOpt = null;
|
||||
for (String s : args) {
|
||||
if (needArgOpt != null) {
|
||||
Field field = needArgOpt.field;
|
||||
Class clazz = field.getType();
|
||||
if (clazz.equals(List.class)) {
|
||||
try {
|
||||
List<Object> object = ((List<Object>) field.get(this));
|
||||
|
||||
// 获取List对象的泛型类型
|
||||
ParameterizedType listGenericType = (ParameterizedType) field.getGenericType();
|
||||
Type[] listActualTypeArguments = listGenericType.getActualTypeArguments();
|
||||
Class typeClazz = (Class) listActualTypeArguments[0];
|
||||
object.add(convert(s, typeClazz));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
field.set(this, convert(s, clazz));
|
||||
}
|
||||
needArgOpt = null;
|
||||
} else if (s.startsWith("-")) {// its a short or long option
|
||||
Option opt = optMap.get(s);
|
||||
requiredOpts.remove(opt);
|
||||
if (opt == null) {
|
||||
System.err.println("ERROR: Unrecognized option: " + s);
|
||||
throw new HelpException();
|
||||
} else {
|
||||
if (opt.hasArg) {
|
||||
needArgOpt = opt;
|
||||
} else {
|
||||
opt.field.set(this, true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
remainsOptions.add(s);
|
||||
}
|
||||
}
|
||||
|
||||
if (needArgOpt != null) {
|
||||
System.err.println("ERROR: Option " + needArgOpt.getOptAndLongOpt() + " need an argument value");
|
||||
throw new HelpException();
|
||||
}
|
||||
this.remainingArgs = remainsOptions.toArray(new String[remainsOptions.size()]);
|
||||
if (this.printHelp) {
|
||||
throw new HelpException();
|
||||
}
|
||||
if (!requiredOpts.isEmpty()) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("ERROR: Options: ");
|
||||
boolean first = true;
|
||||
for (Option option : requiredOpts) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
sb.append(" and ");
|
||||
}
|
||||
sb.append(option.getOptAndLongOpt());
|
||||
}
|
||||
sb.append(" is required");
|
||||
System.err.println(sb.toString());
|
||||
throw new HelpException();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
protected Object convert(String value, Class type) {
|
||||
if (type.equals(String.class)) {
|
||||
return value;
|
||||
}
|
||||
if (type.equals(int.class) || type.equals(Integer.class)) {
|
||||
return Integer.parseInt(value);
|
||||
}
|
||||
if (type.equals(long.class) || type.equals(Long.class)) {
|
||||
return Long.parseLong(value);
|
||||
}
|
||||
if (type.equals(float.class) || type.equals(Float.class)) {
|
||||
return Float.parseFloat(value);
|
||||
}
|
||||
if (type.equals(double.class) || type.equals(Double.class)) {
|
||||
return Double.parseDouble(value);
|
||||
}
|
||||
if (type.equals(boolean.class) || type.equals(Boolean.class)) {
|
||||
return Boolean.parseBoolean(value);
|
||||
}
|
||||
if (type.equals(File.class)) {
|
||||
return new File(value);
|
||||
}
|
||||
if (type.equals(Path.class)) {
|
||||
return new File(value).toPath();
|
||||
}
|
||||
try {
|
||||
type.asSubclass(Enum.class);
|
||||
return Enum.valueOf(type, value);
|
||||
} catch (Exception e) {
|
||||
}
|
||||
|
||||
throw new RuntimeException("can't convert [" + value + "] to type " + type);
|
||||
}
|
||||
|
||||
private Set<Option> collectRequriedOptions(Map<String, Option> optMap) {
|
||||
Set<Option> options = new HashSet<Option>();
|
||||
for (Map.Entry<String, Option> e : optMap.entrySet()) {
|
||||
Option option = e.getValue();
|
||||
if (option.required) {
|
||||
options.add(option);
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
protected static class HelpException extends RuntimeException {
|
||||
|
||||
public HelpException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public HelpException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected void usage() {
|
||||
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.err, StandardCharsets.UTF_8), true);
|
||||
|
||||
final int maxLength = 80;
|
||||
final int maxPaLength = 40;
|
||||
// out.println(this.cmdName + " -- " + desc);
|
||||
// out.println("usage: " + this.cmdName + " " + cmdLineSyntax);
|
||||
if (this.optMap.size() > 0) {
|
||||
out.println("options:");
|
||||
}
|
||||
// [PART.A.........][Part.B
|
||||
// .-a,--aa.<arg>...desc1
|
||||
// .................desc2
|
||||
// .-b,--bb
|
||||
TreeSet<Option> options = new TreeSet<Option>(this.optMap.values());
|
||||
int palength = -1;
|
||||
for (Option option : options) {
|
||||
int pa = 4 + option.getOptAndLongOpt().length();
|
||||
if (option.hasArg) {
|
||||
pa += 3 + option.argName.length();
|
||||
}
|
||||
if (pa < maxPaLength) {
|
||||
if (pa > palength) {
|
||||
palength = pa;
|
||||
}
|
||||
}
|
||||
}
|
||||
int pblength = maxLength - palength;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Option option : options) {
|
||||
sb.setLength(0);
|
||||
sb.append(" ").append(option.getOptAndLongOpt());
|
||||
if (option.hasArg) {
|
||||
sb.append(" <").append(option.argName).append(">");
|
||||
}
|
||||
String desc = option.description;
|
||||
if (desc == null || desc.length() == 0) {// no description
|
||||
out.println(sb);
|
||||
} else {
|
||||
for (int i = palength - sb.length(); i > 0; i--) {
|
||||
sb.append(' ');
|
||||
}
|
||||
if (sb.length() > maxPaLength) {// to huge part A
|
||||
out.println(sb);
|
||||
sb.setLength(0);
|
||||
for (int i = 0; i < palength; i++) {
|
||||
sb.append(' ');
|
||||
}
|
||||
}
|
||||
int nextStart = 0;
|
||||
while (nextStart < desc.length()) {
|
||||
if (desc.length() - nextStart < pblength) {// can put in one line
|
||||
sb.append(desc.substring(nextStart));
|
||||
out.println(sb);
|
||||
nextStart = desc.length();
|
||||
sb.setLength(0);
|
||||
} else {
|
||||
sb.append(desc.substring(nextStart, nextStart + pblength));
|
||||
out.println(sb);
|
||||
nextStart += pblength;
|
||||
sb.setLength(0);
|
||||
if (nextStart < desc.length()) {
|
||||
for (int i = 0; i < palength; i++) {
|
||||
sb.append(' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sb.length() > 0) {
|
||||
out.println(sb);
|
||||
sb.setLength(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
String ver = getVersionString();
|
||||
if (ver != null && !"".equals(ver)) {
|
||||
out.println("version: " + ver);
|
||||
}
|
||||
if (onlineHelp != null && !"".equals(onlineHelp)) {
|
||||
if (onlineHelp.length() + "online help: ".length() > maxLength) {
|
||||
out.println("online help: ");
|
||||
out.println(onlineHelp);
|
||||
} else {
|
||||
out.println("online help: " + onlineHelp);
|
||||
}
|
||||
}
|
||||
out.flush();
|
||||
}
|
||||
|
||||
public static String getBaseName(String fn) {
|
||||
int x = fn.lastIndexOf('.');
|
||||
return x >= 0 ? fn.substring(0, x) : fn;
|
||||
}
|
||||
|
||||
// 获取文件不包含后缀的名称
|
||||
public static String getBaseName(File fn) {
|
||||
return getBaseName(fn.getName());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ public class FileUtils {
|
|||
copyFile(new File(sourcePath), new File(targetPath));
|
||||
}
|
||||
|
||||
public static void copyFile(File source, File target) {
|
||||
public static boolean copyFile(File source, File target) {
|
||||
|
||||
FileInputStream inputStream = null;
|
||||
FileOutputStream outputStream = null;
|
||||
|
|
@ -146,12 +146,14 @@ public class FileUtils {
|
|||
buffer.position(0);
|
||||
oChannel.write(buffer);
|
||||
}
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
close(inputStream);
|
||||
close(outputStream);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void deleteDir(File file) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue