diff --git a/README.md b/README.md index 6aa8afba..a22db76c 100755 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # APKEditor ### Powerful android apk resources editor -This tool uses [ARSCLib](https://github.com/REAndroid/ARSCLib) to edit any apk resources and has five main features +This tool uses [ARSCLib](https://github.com/REAndroid/ARSCLib) to edit any apk resources and has six main features
java -jar APKEditor.jar -h @@ -20,6 +20,7 @@ Usage: 3) m | merge - Merges split apk files from directory or XAPK, APKM, APKS ... 4) x | refactor - Refactors obfuscated resource names 5) p | protect - Protects/Obfuscates apk resource + 6) info - Prints information of apk run with -h to get detailed help about each command ``` @@ -122,7 +123,7 @@ $ java -jar APKEditor.jar x -i input.apk
-#### 5- Protect (⭐NEW⭐) +#### 5- Protect Protects apk resources against almost all known decompile/modify tools.
java -jar APKEditor.jar p -i path/to/input.apk @@ -143,7 +144,30 @@ Protects apk resources against almost all known decompile/modify tools. ```
- + +#### 6- Info (⭐NEW⭐) +Prints/dumps from basic up to detailed information of apk. +
java -jar APKEditor.jar info -v -resources -i input.apk + + ```ShellSession +Package name=com.mypackage id=0x7f + type string id=1 entryCount=1 + resource 0x7f010000 string/app_name + () My Application + (-de) Meine Bewerbung + (-ru-rRU) Мое заявление + type mipmap id=2 entryCount=1 + resource 0x7f020000 mipmap/ic_launcher_round + () res/mipmap/ic_launcher_round.png + type drawable id=3 entryCount=1 + resource 0x7f030000 drawable/ic_launcher + () #006400 + +``` + +
+ + --- ***Build executable jar*** diff --git a/libs/ARSCLib.jar b/libs/ARSCLib.jar index 4e7b1352..b00679a3 100644 Binary files a/libs/ARSCLib.jar and b/libs/ARSCLib.jar differ diff --git a/src/main/java/com/reandroid/apkeditor/BaseCommand.java b/src/main/java/com/reandroid/apkeditor/BaseCommand.java index 7209ec9e..b9ef0d5c 100644 --- a/src/main/java/com/reandroid/apkeditor/BaseCommand.java +++ b/src/main/java/com/reandroid/apkeditor/BaseCommand.java @@ -1,31 +1,81 @@ - /* - * Copyright (C) 2022 github.com/REAndroid - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.reandroid.apkeditor; +import com.reandroid.apk.APKLogger; import com.reandroid.archive.APKArchive; import com.reandroid.apk.ApkModule; +import com.reandroid.commons.utils.log.Logger; +import java.io.IOException; import java.util.regex.Pattern; - public class BaseCommand { +public class BaseCommand implements APKLogger { + private String mLogTag; + private boolean mEnableLog; + public BaseCommand(){ + mLogTag = ""; + mEnableLog = true; + } + public void run() throws IOException{ - protected static void removeSignature(ApkModule module){ - APKArchive archive = module.getApkArchive(); - archive.removeAll(Pattern.compile("^META-INF/(?!services/).*")); - archive.remove("stamp-cert-sha256"); - } + } + + protected void setLogTag(String tag) { + if(tag == null){ + tag = ""; + } + this.mLogTag = tag; + } + public void setEnableLog(boolean enableLog) { + this.mEnableLog = enableLog; + } + @Override + public void logMessage(String msg) { + if(!mEnableLog){ + return; + } + Logger.i(mLogTag + msg); + } + @Override + public void logError(String msg, Throwable tr) { + if(!mEnableLog){ + return; + } + Logger.e(mLogTag + msg, tr); + } + @Override + public void logVerbose(String msg) { + if(!mEnableLog){ + return; + } + Logger.sameLine(mLogTag + msg); + } + public void logWarn(String msg) { + Logger.e(mLogTag + msg); + } + + protected static void clearMeta(ApkModule module){ + removeSignature(module); + module.setApkSignatureBlock(null); + } + protected static void removeSignature(ApkModule module){ + APKArchive archive = module.getApkArchive(); + archive.removeAll(Pattern.compile("^META-INF/.+\\.(([MS]F)|(RSA))")); + archive.remove("stamp-cert-sha256"); + } } diff --git a/src/main/java/com/reandroid/apkeditor/Main.java b/src/main/java/com/reandroid/apkeditor/Main.java index 6a98a4c6..a91902af 100644 --- a/src/main/java/com/reandroid/apkeditor/Main.java +++ b/src/main/java/com/reandroid/apkeditor/Main.java @@ -1,25 +1,28 @@ - /* - * Copyright (C) 2022 github.com/REAndroid - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.reandroid.apkeditor; +import com.reandroid.apkeditor.cloner.Cloner; import com.reandroid.apkeditor.compile.Builder; import com.reandroid.apkeditor.decompile.Decompiler; +import com.reandroid.apkeditor.info.Info; import com.reandroid.apkeditor.merge.Merger; import com.reandroid.apkeditor.protect.Protector; import com.reandroid.apkeditor.refactor.Refactor; +import com.reandroid.apkeditor.utils.StringHelper; import com.reandroid.commons.command.ARGException; import com.reandroid.apk.xmlencoder.EncodeException; @@ -77,6 +80,14 @@ private static void execute(String command, String[] args) throws ARGException, Protector.execute(args); return; } + if(Cloner.isCommand(command)){ + Cloner.execute(args); + return; + } + if(Info.isCommand(command)){ + Info.execute(args); + return; + } throw new ARGException("Unknown command: "+command); } private static String getHelp(){ @@ -85,18 +96,21 @@ private static String getHelp(){ builder.append("\nUsage: \n"); builder.append(" java -jar ").append(APKEditor.getJarName()); builder.append(" "); - builder.append("\n commands: "); - builder.append("\n 1) ").append(Decompiler.ARG_SHORT).append(" | ").append(Decompiler.ARG_LONG); - builder.append(" - ").append(Decompiler.DESCRIPTION); - builder.append("\n 2) ").append(Builder.ARG_SHORT).append(" | ").append(Builder.ARG_LONG); - builder.append(" - ").append(Builder.DESCRIPTION); - builder.append("\n 3) ").append(Merger.ARG_SHORT).append(" | ").append(Merger.ARG_LONG); - builder.append(" - ").append(Merger.DESCRIPTION); - builder.append("\n 4) ").append(Refactor.ARG_SHORT).append(" | ").append(Refactor.ARG_LONG); - builder.append(" - ").append(Refactor.DESCRIPTION); - builder.append("\n 5) ").append(Protector.ARG_SHORT).append(" | ").append(Protector.ARG_LONG); - builder.append(" - ").append(Protector.DESCRIPTION); - builder.append("\n run with -h to get detailed help about each command"); + builder.append("\n commands: \n"); + String[][] table = new String[][]{ + new String[]{" 1) " + Decompiler.ARG_SHORT + " | " + Decompiler.ARG_LONG, Decompiler.DESCRIPTION}, + new String[]{" 2) " + Builder.ARG_SHORT + " | " + Builder.ARG_LONG, Builder.DESCRIPTION}, + new String[]{" 3) " + Merger.ARG_SHORT + " | " + Merger.ARG_LONG, Merger.DESCRIPTION}, + new String[]{" 4) " + Refactor.ARG_SHORT + " | " + Refactor.ARG_LONG, Refactor.DESCRIPTION}, + new String[]{" 5) " + Protector.ARG_SHORT + " | " + Protector.ARG_LONG, Protector.DESCRIPTION}, + //new String[]{" 6) " + Cloner.ARG_SHORT + " | " + Cloner.ARG_LONG, Cloner.DESCRIPTION}, + new String[]{" 6) " + Info.ARG_SHORT, Info.DESCRIPTION} + }; + + StringHelper.printTwoColumns(builder, " ", " - ", Options.PRINT_WIDTH, table); + + builder.append("\n\n run with -h to get detailed help about each command\n"); + return builder.toString(); } private static String getWelcome(){ diff --git a/src/main/java/com/reandroid/apkeditor/Options.java b/src/main/java/com/reandroid/apkeditor/Options.java index a3180cd6..a48d2cd4 100644 --- a/src/main/java/com/reandroid/apkeditor/Options.java +++ b/src/main/java/com/reandroid/apkeditor/Options.java @@ -46,18 +46,8 @@ protected void parseFrameworkVersion(String[] args) throws ARGException { } } protected void parseType(String[] args) throws ARGException { - this.type = parseArgValue(ARG_type, true, args); - if(type == null){ - type = TYPE_JSON; - return; - } - type = type.trim().toLowerCase(); - if(TYPE_JSON.equals(type) - || TYPE_XML.equals(type) - || TYPE_SIG.equals(type)){ - return; - } - throw new ARGException("Unknown decompile type: "+type); + String[] choices = new String[]{TYPE_JSON, TYPE_XML, TYPE_SIG}; + this.type = parseType(ARG_type, args, choices, TYPE_JSON); } protected void parseSignaturesDir(String[] args) throws ARGException { this.signaturesDirectory = parseFile(ARG_sig, args); @@ -72,6 +62,32 @@ protected void checkUnknownOptions(String[] args) throws ARGException { } throw new ARGException("Unknown option: "+args[0]); } + + protected String parseType(String argSwitch, String[] args, String[] availableTypes, String def) throws ARGException { + String type = parseArgValue(argSwitch, args); + if(type == null){ + return def; + } + type = type.trim(); + String typeLower = type.toLowerCase(); + for(String choice : availableTypes){ + if(typeLower.equals(choice)){ + return typeLower; + } + } + StringBuilder builder = new StringBuilder(); + builder.append("Unknown type: '"); + builder.append(type); + builder.append("' , must be one of {"); + for(int i = 0; i < availableTypes.length; i++){ + if(i != 0){ + builder.append(", "); + } + builder.append(availableTypes[i]); + } + builder.append("}"); + throw new ARGException(builder.toString()); + } protected String parseArgValue(String argSwitch, String[] args) throws ARGException { return parseArgValue(argSwitch, true, args); } @@ -132,7 +148,16 @@ protected File parseFile(String argSwitch, String[] args) throws ARGException { } return null; } - protected boolean containsArg(String argSwitch, boolean ignore_case, String[] args) throws ARGException { + protected boolean containsArg(String argSwitch, String[] args) { + return containsArg(argSwitch, true, args, false); + } + protected boolean containsArg(String argSwitch, String[] args, boolean def) { + return containsArg(argSwitch, true, args, def); + } + protected boolean containsArg(String argSwitch, boolean ignore_case, String[] args){ + return containsArg(argSwitch, ignore_case, args, false); + } + protected boolean containsArg(String argSwitch, boolean ignore_case, String[] args, boolean def) { if(ignore_case){ argSwitch=argSwitch.toLowerCase(); } @@ -151,9 +176,14 @@ protected boolean containsArg(String argSwitch, boolean ignore_case, String[] ar return true; } } - return false; + return def; } + public static final int PRINT_WIDTH = 75; + + protected static final String ARG_ALL_help = "-h|-help"; + protected static final String ARG_DESC_help = "Prints this help"; + protected static final String ARG_output="-o"; protected static final String ARG_DESC_output="output path"; protected static final String ARG_input="-i"; @@ -173,12 +203,10 @@ protected boolean containsArg(String argSwitch, boolean ignore_case, String[] ar protected static final String ARG_DESC_framework_version = "preferred framework version number"; public static final String ARG_type = "-t"; - public static final String ARG_DESC_type = "Decode types: \n1) json \n2) xml \n3) sig \n default=json" + - "\n * Output directory contains \n a) res package directory(s) name={index number}-{package name}" + - "\n b) root: directory of raw files like dex, assets, lib ... \n c) AndroidManifest.xml"; - public static final String TYPE_SIG = "sig"; public static final String TYPE_JSON = "json"; public static final String TYPE_XML = "xml"; + public static final String TYPE_TEXT = "text"; + protected static final String LINE = " ------------------------------------------------------------------------"; } diff --git a/src/main/java/com/reandroid/apkeditor/Util.java b/src/main/java/com/reandroid/apkeditor/Util.java index 3d25e160..a87e3664 100644 --- a/src/main/java/com/reandroid/apkeditor/Util.java +++ b/src/main/java/com/reandroid/apkeditor/Util.java @@ -1,44 +1,59 @@ - /* - * Copyright (C) 2022 github.com/REAndroid - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.reandroid.apkeditor; import com.reandroid.apk.ApkModule; -import com.reandroid.arsc.chunk.PackageBlock; import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.arsc.item.IntegerItem; import com.reandroid.arsc.item.TableString; import com.reandroid.arsc.pool.TableStringPool; import java.io.File; -import java.io.IOException; import java.io.StringReader; import java.util.ArrayList; import java.util.List; import java.util.Properties; - public class Util { +public class Util { public static boolean isHelp(String[] args){ if(isEmpty(args)){ return true; } - String command=args[0]; + return isHelp(args[0]); + } + public static boolean containsHelp(String[] args){ + if(isEmpty(args)){ + return false; + } + for(String command : args){ + if(isHelp(command)){ + return true; + } + } + return false; + } + public static boolean isHelp(String command){ + if(isEmpty(command)){ + return false; + } command=command.toLowerCase().trim(); return command.equals("-h") ||command.equals("-help") - ||command.equals("h") - ||command.equals("help"); + ||command.equals("--h") + ||command.equals("--help"); } public static String[] trimNull(String[] args){ if(isEmpty(args)){ @@ -184,6 +199,9 @@ private static void addApkEditorInfo(TableBlock tableBlock, String type){ tableString = stringPool.get(count); } tableString.set(buildApkEditorInfo(type)); + IntegerItem dummyReference = new IntegerItem(); + dummyReference.set(tableString.getIndex()); + tableString.addReference(dummyReference); } private static String buildApkEditorInfo(String type){ StringBuilder builder = new StringBuilder(); diff --git a/src/main/java/com/reandroid/apkeditor/cloner/Cloner.java b/src/main/java/com/reandroid/apkeditor/cloner/Cloner.java new file mode 100644 index 00000000..4b0718c5 --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/cloner/Cloner.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.apkeditor.cloner; + +import com.reandroid.apkeditor.APKEditor; +import com.reandroid.apkeditor.BaseCommand; +import com.reandroid.apkeditor.Util; +import com.reandroid.commons.command.ARGException; + +import java.io.IOException; + +public class Cloner extends BaseCommand { + private final ClonerOptions options; + public Cloner(ClonerOptions options){ + this.options = options; + } + @Override + public void run() throws IOException{ + logWarn("This feature not implemented, follow updates on: " + APKEditor.getRepo()); + } + + public static void execute(String[] args) throws ARGException, IOException { + if(Util.isHelp(args)){ + throw new ARGException(ClonerOptions.getHelp()); + } + ClonerOptions option = new ClonerOptions(); + option.parse(args); + Cloner cloner = new Cloner(option); + cloner.run(); + } + + public static boolean isCommand(String command){ + if(Util.isEmpty(command)){ + return false; + } + command = command.toLowerCase().trim(); + return command.equals(ARG_LONG); + //return command.equals(ARG_SHORT) || command.equals(ARG_LONG); + } + + public static final String ARG_SHORT = "c"; + public static final String ARG_LONG = "clone"; + + public static final String DESCRIPTION = "Clones apk"; +} diff --git a/src/main/java/com/reandroid/apkeditor/cloner/ClonerOptions.java b/src/main/java/com/reandroid/apkeditor/cloner/ClonerOptions.java new file mode 100644 index 00000000..6f112aad --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/cloner/ClonerOptions.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.apkeditor.cloner; + +import com.reandroid.apkeditor.APKEditor; +import com.reandroid.apkeditor.Options; +import com.reandroid.apkeditor.Util; +import com.reandroid.apkeditor.utils.StringHelper; +import com.reandroid.commons.command.ARGException; + +import java.io.File; + +public class ClonerOptions extends Options { + public String packageName; + public String appName; + public String appIcon; + public boolean keepAuth; + public ClonerOptions(){ + super(); + } + @Override + public void parse(String[] args) throws ARGException { + parseInput(args); + + packageName = parseArgValue(ARG_package, args); + appName = parseArgValue(ARG_app_name, args); + appIcon = parseArgValue(ARG_app_icon, args); + keepAuth = containsArg(ARG_app_icon, args); + + parseHelp(args); + + checkUnknownOptions(args); + } + private void parseInput(String[] args) throws ARGException { + this.inputFile = null; + File file = parseFile(ARG_input, args); + if(file == null){ + throw new ARGException("Missing input apk! Specify with " + ARG_input); + } + if(!file.isFile()){ + throw new ARGException("No such file: "+file); + } + this.inputFile = file; + } + private void parseHelp(String[] args) throws ARGException { + if(!Util.containsHelp(args)){ + return; + } + throw new ARGException(getHelp()); + } + + public static String getHelp(){ + StringBuilder builder=new StringBuilder(); + builder.append(Cloner.DESCRIPTION); + builder.append("\nOptions:\n"); + String[][] table=new String[][]{ + new String[]{ARG_input, ARG_DESC_input}, + new String[]{ARG_output, ARG_DESC_output}, + new String[]{ARG_package, ARG_DESC_package}, + new String[]{ARG_app_name, ARG_DESC_app_name}, + new String[]{ARG_app_icon, ARG_DESC_app_icon} + }; + StringHelper.printTwoColumns(builder, " ", PRINT_WIDTH, table); + builder.append("\n\nFlags:\n"); + table=new String[][]{ + new String[]{ARG_keep_auth, ARG_DESC_keep_auth}, + new String[]{" ", " "}, + new String[]{ARG_ALL_help, ARG_DESC_help} + }; + StringHelper.printTwoColumns(builder, " ", PRINT_WIDTH, table); + + builder.append("\n").append(Options.LINE); + + String jar = APKEditor.getJarName(); + + builder.append("\n\nExample-1:"); + builder.append("\n java -jar ").append(jar).append(" ").append(Cloner.ARG_SHORT).append(" ") + .append(ARG_input).append(" file.apk"); + + return builder.toString(); + } + + private static final String ARG_package = "-package"; + private static final String ARG_DESC_package = "Package name."; + + private static final String ARG_app_name = "-app-name"; + private static final String ARG_DESC_app_name = "Application name."; + + private static final String ARG_app_icon = "-app-icon"; + private static final String ARG_DESC_app_icon = "Application icon. File path of app icon(s)."; + + private static final String ARG_keep_auth = "-keep-auth"; + private static final String ARG_DESC_keep_auth = "Do not rename authorities as per package. \n *Applies only when option -package used."; + +} diff --git a/src/main/java/com/reandroid/apkeditor/common/AndroidManifestHelper.java b/src/main/java/com/reandroid/apkeditor/common/AndroidManifestHelper.java index a0c6454f..0e5f892c 100644 --- a/src/main/java/com/reandroid/apkeditor/common/AndroidManifestHelper.java +++ b/src/main/java/com/reandroid/apkeditor/common/AndroidManifestHelper.java @@ -1,4 +1,4 @@ - /* +/* * Copyright (C) 2022 github.com/REAndroid * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,11 +15,9 @@ */ package com.reandroid.apkeditor.common; -import com.reandroid.arsc.array.ResXmlAttributeArray; import com.reandroid.arsc.chunk.xml.AndroidManifestBlock; import com.reandroid.arsc.chunk.xml.ResXmlAttribute; import com.reandroid.arsc.chunk.xml.ResXmlElement; -import com.reandroid.arsc.chunk.xml.ResXmlStartElement; import com.reandroid.arsc.value.ValueType; import java.util.ArrayList; @@ -27,22 +25,20 @@ public class AndroidManifestHelper { public static List listSplitRequired(ResXmlElement parentElement){ - List results=new ArrayList<>(); - if(parentElement==null){ + List results = new ArrayList<>(); + if(parentElement == null){ return results; } - List metaDataList = parentElement.listElements(AndroidManifestBlock.TAG_meta_data); + List metaDataList = parentElement + .listElements(AndroidManifestBlock.TAG_meta_data); + for(ResXmlElement metaData:metaDataList){ - ResXmlAttribute nameAttribute = metaData.getStartElement() - .getAttribute(AndroidManifestBlock.ID_name); - if(nameAttribute==null){ + ResXmlAttribute nameAttribute = metaData + .searchAttributeByResourceId(AndroidManifestBlock.ID_name); + if(nameAttribute == null){ continue; } if(nameAttribute.getValueType() != ValueType.STRING){ - /* - * TODO: could be reference , - * thus we need TableBlock/EntryStore to resolve string value. - */ continue; } String value = nameAttribute.getValueAsString(); @@ -54,18 +50,16 @@ public static List listSplitRequired(ResXmlElement parentElement) return results; } public static boolean removeApplicationAttribute(AndroidManifestBlock manifest, int resId){ - ResXmlElement app = manifest.getApplicationElement(); - if(app==null){ + ResXmlElement applicationElement = manifest.getApplicationElement(); + if(applicationElement == null){ return true; } - ResXmlStartElement start = app.getStartElement(); - ResXmlAttribute attr = start.getAttribute(resId); - if(attr==null){ + ResXmlAttribute attribute = applicationElement + .searchAttributeByResourceId(resId); + if(attribute == null){ return false; } - ResXmlAttributeArray array = start.getResXmlAttributeArray(); - array.remove(attr); - manifest.refresh(); + applicationElement.removeAttribute(attribute); return true; } } diff --git a/src/main/java/com/reandroid/apkeditor/compile/BuildOptions.java b/src/main/java/com/reandroid/apkeditor/compile/BuildOptions.java index 1c8f054e..2cd63973 100644 --- a/src/main/java/com/reandroid/apkeditor/compile/BuildOptions.java +++ b/src/main/java/com/reandroid/apkeditor/compile/BuildOptions.java @@ -117,13 +117,13 @@ public static String getHelp(){ new String[]{ARG_sig, ARG_DESC_sig}, new String[]{ARG_resDir, ARG_DESC_resDir} }; - StringHelper.printTwoColumns(builder, " ", 75, table); + StringHelper.printTwoColumns(builder, " ", Options.PRINT_WIDTH, table); builder.append("\nFlags:\n"); table=new String[][]{ new String[]{ARG_force, ARG_DESC_force}, new String[]{ARG_validate_res_dir, ARG_DESC_validate_res_dir} }; - StringHelper.printTwoColumns(builder, " ", 75, table); + StringHelper.printTwoColumns(builder, " ", Options.PRINT_WIDTH, table); String jar = APKEditor.getJarName(); builder.append("\n\nExample-1:"); builder.append("\n java -jar ").append(jar).append(" ").append(Builder.ARG_SHORT).append(" ") diff --git a/src/main/java/com/reandroid/apkeditor/decompile/DecompileOptions.java b/src/main/java/com/reandroid/apkeditor/decompile/DecompileOptions.java index 681bf9ed..16358237 100644 --- a/src/main/java/com/reandroid/apkeditor/decompile/DecompileOptions.java +++ b/src/main/java/com/reandroid/apkeditor/decompile/DecompileOptions.java @@ -128,14 +128,14 @@ public static String getHelp(){ new String[]{ARG_type, ARG_DESC_type}, new String[]{ARG_resDir, ARG_DESC_resDir} }; - StringHelper.printTwoColumns(builder, " ", 75, table); + StringHelper.printTwoColumns(builder, " ", Options.PRINT_WIDTH, table); builder.append("\nFlags:\n"); table=new String[][]{ new String[]{ARG_force, ARG_DESC_force}, new String[]{ARG_split_resources, ARG_DESC_split_resources}, new String[]{ARG_validate_res_dir, ARG_DESC_validate_res_dir} }; - StringHelper.printTwoColumns(builder, " ", 75, table); + StringHelper.printTwoColumns(builder, " ", Options.PRINT_WIDTH, table); String jar = APKEditor.getJarName(); builder.append("\n\nExample-1:"); builder.append("\n java -jar ").append(jar).append(" ").append(Builder.ARG_SHORT).append(" ") diff --git a/src/main/java/com/reandroid/apkeditor/info/Info.java b/src/main/java/com/reandroid/apkeditor/info/Info.java new file mode 100644 index 00000000..7c0ed3ae --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/info/Info.java @@ -0,0 +1,421 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.apkeditor.info; + +import com.reandroid.apk.ApkModule; +import com.reandroid.apkeditor.BaseCommand; +import com.reandroid.apkeditor.Util; +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.arsc.chunk.xml.AndroidManifestBlock; +import com.reandroid.arsc.chunk.xml.ResXmlAttribute; +import com.reandroid.arsc.chunk.xml.ResXmlElement; +import com.reandroid.arsc.coder.EncodeResult; +import com.reandroid.arsc.coder.ReferenceString; +import com.reandroid.arsc.coder.ValueCoder; +import com.reandroid.arsc.group.EntryGroup; +import com.reandroid.arsc.util.HexUtil; +import com.reandroid.arsc.value.AttributeDataFormat; +import com.reandroid.arsc.value.Entry; +import com.reandroid.arsc.value.ResValue; +import com.reandroid.arsc.value.ValueType; +import com.reandroid.commons.command.ARGException; + +import java.io.*; +import java.util.*; + +public class Info extends BaseCommand { + private final InfoOptions options; + private InfoWriter mInfoWriter; + public Info(InfoOptions options){ + super(); + this.options = options; + super.setLogTag(LOG_TAG_INFO); + super.setEnableLog(options.outputFile != null); + } + @Override + public void run() throws IOException{ + setEnableLog(options.outputFile != null); + logMessage("Loading: " + options.inputFile); + ApkModule apkModule = ApkModule.loadApkFile(options.inputFile); + String msg = Util.isProtected(apkModule); + if(msg != null){ + logMessage(msg); + return; + } + apkModule.setAPKLogger(this); + apkModule.setLoadDefaultFramework(options.verbose); + File out = options.outputFile; + if(out != null){ + logMessage("Writing ..."); + } + print(apkModule); + flush(); + close(); + if(out != null){ + logMessage("Saved to: " + out); + } + } + private void print(ApkModule apkModule) throws IOException { + printSourceFile(); + + printPackage(apkModule); + AndroidManifestBlock manifest = apkModule.getAndroidManifestBlock(); + printVersionCode(manifest); + printVersionName(manifest); + + printAppName(apkModule); + printAppIcon(apkModule); + printAppRoundIcon(apkModule); + printAppClass(apkModule); + printActivities(apkModule); + printUsesPermissions(apkModule); + + printResList(apkModule); + + printResources(apkModule); + } + private void printSourceFile() throws IOException { + if(options.outputFile == null){ + return; + } + if(options.verbose || !options.resources){ + getInfoWriter().writeNameValue("source-file", + options.inputFile.getAbsolutePath()); + } + } + private void printResources(ApkModule apkModule) throws IOException { + if(!options.resources){ + return; + } + if(!apkModule.hasTableBlock()){ + return; + } + TableBlock tableBlock = apkModule.getTableBlock(); + InfoWriter infoWriter = getInfoWriter(); + boolean writeEntries = options.verbose; + for(PackageBlock packageBlock : tableBlock.listPackages()){ + infoWriter.writeResources(packageBlock, options.typeFilterList, writeEntries); + } + } + private void printResList(ApkModule apkModule) throws IOException { + if(options.resList.size() == 0){ + return; + } + if(!apkModule.hasTableBlock()){ + return; + } + for(String res : options.resList){ + printRes(apkModule, res); + } + } + private void printRes(ApkModule apkModule, String res) throws IOException { + if(res == null || res.length() < 3){ + return; + } + if(res.startsWith("@0x")){ + res = res.substring(1); + } + EncodeResult encodeResult = ValueCoder.encode(res, AttributeDataFormat.INTEGER.valueTypes()); + if(encodeResult != null){ + printEntries(apkModule, "resource", encodeResult.value); + return; + } + ReferenceString referenceString = ReferenceString.parseReference(res); + if(referenceString == null){ + logWarn("WARN: Invalid resource: " + res); + return; + } + TableBlock tableBlock = apkModule.getTableBlock(); + for(PackageBlock packageBlock : tableBlock.listPackages()){ + EntryGroup entryGroup = packageBlock + .getEntryGroup(referenceString.type, referenceString.name); + if(entryGroup == null){ + continue; + } + printEntries(apkModule, "resource", entryGroup.getResourceId()); + return; + } + logMessage("WARN: resource not found: " + res); + } + private void printPackage(ApkModule apkModule) throws IOException { + if(!options.packageName){ + return; + } + AndroidManifestBlock manifest = apkModule.getAndroidManifestBlock(); + if(manifest != null){ + getInfoWriter().writeNameValue("package" , manifest.getPackageName()); + } + if(!options.verbose || !apkModule.hasTableBlock()){ + return; + } + TableBlock tableBlock = apkModule.getTableBlock(); + InfoWriter infoWriter = getInfoWriter(); + infoWriter.writePackageNames(tableBlock.listPackages()); + } + private void printVersionCode(AndroidManifestBlock manifest) throws IOException { + if(!options.versionCode || manifest == null){ + return; + } + getInfoWriter().writeNameValue("VersionCode" , manifest.getVersionCode()); + } + private void printVersionName(AndroidManifestBlock manifest) throws IOException { + if(!options.versionName || manifest == null){ + return; + } + getInfoWriter().writeNameValue("VersionName" , manifest.getVersionName()); + } + private void printAppName(ApkModule apkModule) throws IOException { + if(!options.appName){ + return; + } + AndroidManifestBlock manifest = apkModule.getAndroidManifestBlock(); + ResXmlElement application = manifest.getApplicationElement(); + ResXmlAttribute attributeLabel = application + .searchAttributeByResourceId(AndroidManifestBlock.ID_label); + if(attributeLabel == null){ + return; + } + if(attributeLabel.getValueType() == ValueType.STRING){ + getInfoWriter().writeNameValue("AppName", attributeLabel.getValueAsString()); + return; + } + int resourceId = attributeLabel.getData(); + printEntries(apkModule, "AppName", resourceId); + } + private void printAppIcon(ApkModule apkModule) throws IOException { + if(!options.appIcon){ + return; + } + AndroidManifestBlock manifest = apkModule.getAndroidManifestBlock(); + ResXmlElement application = manifest.getApplicationElement(); + ResXmlAttribute attribute = application + .searchAttributeByResourceId(AndroidManifestBlock.ID_icon); + if(attribute == null){ + return; + } + if(attribute.getValueType() == ValueType.STRING){ + getInfoWriter().writeNameValue("AppIcon", attribute.getValueAsString()); + return; + } + int resourceId = attribute.getData(); + printEntries(apkModule, "AppIcon", resourceId); + } + private void printAppRoundIcon(ApkModule apkModule) throws IOException { + if(!options.appRoundIcon){ + return; + } + AndroidManifestBlock manifest = apkModule.getAndroidManifestBlock(); + ResXmlElement application = manifest.getApplicationElement(); + int id_roundIcon = 0x0101052c; + ResXmlAttribute attribute = application + .searchAttributeByResourceId(id_roundIcon); + if(attribute == null){ + return; + } + if(attribute.getValueType() == ValueType.STRING){ + getInfoWriter().writeNameValue("AppRoundIcon", attribute.getValueAsString()); + return; + } + int resourceId = attribute.getData(); + printEntries(apkModule, "AppRoundIcon", resourceId); + } + private void printUsesPermissions(ApkModule apkModule) throws IOException { + if(!options.permissions || !options.verbose){ + return; + } + AndroidManifestBlock manifest = apkModule.getAndroidManifestBlock(); + if(manifest == null){ + return; + } + List usesPermissions = manifest.getUsesPermissions(); + if(usesPermissions.size() == 0){ + return; + } + //printLine("Uses permission (" + usesPermissions.size() + ")"); + String tag = AndroidManifestBlock.TAG_uses_permission; + InfoWriter infoWriter = getInfoWriter(); + infoWriter.writeArray(tag, usesPermissions.toArray(new String[0])); + } + private void printActivities(ApkModule apkModule) throws IOException { + if(!options.activities){ + return; + } + AndroidManifestBlock manifest = apkModule.getAndroidManifestBlock(); + if(manifest == null){ + return; + } + List activityList = manifest.listActivities(true); + if(activityList.size() == 0){ + return; + } + ResXmlElement main = manifest.getMainActivity(); + if(main != null){ + String value = getValueOfName(main); + getInfoWriter().writeNameValue("activity-main", value); + } + if(!options.verbose){ + return; + } + String[] activityNames = new String[activityList.size()]; + for(int i = 0; i < activityList.size(); i++){ + ResXmlElement activity = activityList.get(i); + activityNames[i] = getValueOfName(activity); + } + InfoWriter infoWriter = getInfoWriter(); + infoWriter.writeArray("activities", activityNames); + } + private void printAppClass(ApkModule apkModule) throws IOException { + if(!options.appClass){ + return; + } + AndroidManifestBlock manifest = apkModule.getAndroidManifestBlock(); + if(manifest == null){ + return; + } + ResXmlElement applicationElement = manifest.getApplicationElement(); + if(applicationElement == null){ + return; + } + String value = getValueOfName(applicationElement); + if(value != null){ + getInfoWriter().writeNameValue("application-class", value); + } + } + private String getValueOfName(ResXmlElement element){ + ResXmlAttribute attribute = element + .searchAttributeByResourceId(AndroidManifestBlock.ID_name); + if(attribute == null){ + return null; + } + return attribute.getValueAsString(); + } + private void printEntries(ApkModule apkModule, String varName, int resourceId) throws IOException { + TableBlock tableBlock = apkModule.getTableBlock(); + InfoWriter infoWriter = getInfoWriter(); + if(tableBlock == null){ + infoWriter.writeNameValue(varName, HexUtil.toHex8( "@0x", resourceId)); + return; + } + List entryList = tableBlock.resolveReference(resourceId); + if(entryList.size() == 0){ + logWarn("WARN: Can't find resource: " + HexUtil.toHex8("@0x", resourceId)); + //infoWriter.writeNameValue(varName, HexUtil.toHex8("@0x", resourceId)); + return; + } + entryList = sortEntries(entryList); + if(!options.verbose){ + infoWriter.writeNameValue(varName, getValueAsString(entryList.get(0))); + return; + } + infoWriter.writeEntries(varName, entryList); + } + private String getValueAsString(Entry entry){ + ResValue resValue = entry.getResValue(); + if(resValue == null){ + return ""; + } + ValueType valueType = resValue.getValueType(); + if(valueType == ValueType.STRING){ + return resValue.getValueAsString(); + } + String decoded = ValueCoder.decode(valueType, resValue.getData()); + if(decoded != null){ + return decoded; + } + return HexUtil.toHex8("@0x", resValue.getData()); + } + private InfoWriter getInfoWriter() throws IOException{ + if(mInfoWriter != null){ + return mInfoWriter; + } + Writer writer = createWriter(); + InfoWriter infoWriter; + if(InfoOptions.TYPE_JSON.equals(options.type)){ + infoWriter = new InfoWriterJson(writer); + }else if(InfoOptions.TYPE_XML.equals(options.type)){ + infoWriter = new InfoWriterXml(writer); + }else { + infoWriter = new InfoWriterText(writer); + } + mInfoWriter = infoWriter; + return mInfoWriter; + } + private Writer createWriter() throws IOException{ + File file = options.outputFile; + if(file == null){ + return new PrintWriter(System.out); + } + File dir = file.getParentFile(); + if(dir != null && !dir.exists()){ + dir.mkdirs(); + } + return new OutputStreamWriter(new FileOutputStream(file)); + } + private void flush() throws IOException { + InfoWriter writer = this.mInfoWriter; + if(writer != null){ + writer.flush(); + } + } + private void close() throws IOException { + InfoWriter writer = this.mInfoWriter; + if(writer != null){ + writer.close(); + mInfoWriter = null; + } + } + + public static void execute(String[] args) throws ARGException, IOException { + if(Util.isHelp(args)){ + throw new ARGException(InfoOptions.getHelp()); + } + InfoOptions option = new InfoOptions(); + option.parse(args); + Info info = new Info(option); + info.run(); + } + + private static List sortEntries(Collection entryCollection) { + ArrayList results; + if(entryCollection instanceof ArrayList){ + results = (ArrayList) entryCollection; + }else { + results = new ArrayList<>(entryCollection); + } + Comparator cmp = new Comparator() { + @Override + public int compare(Entry entry1, Entry entry2) { + return entry1.getResConfig().compareTo(entry2.getResConfig()); + } + }; + results.sort(cmp); + return results; + } + + public static boolean isCommand(String command){ + if(Util.isEmpty(command)){ + return false; + } + command=command.toLowerCase().trim(); + return command.equals(ARG_SHORT); + } + + public static final String ARG_SHORT = "info"; + public static final String DESCRIPTION = "Prints information of apk"; + + private static final String LOG_TAG_INFO = "[INFO] "; +} diff --git a/src/main/java/com/reandroid/apkeditor/info/InfoOptions.java b/src/main/java/com/reandroid/apkeditor/info/InfoOptions.java new file mode 100644 index 00000000..d5001f5a --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/info/InfoOptions.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.apkeditor.info; + +import com.reandroid.apkeditor.APKEditor; +import com.reandroid.apkeditor.Options; +import com.reandroid.apkeditor.Util; +import com.reandroid.apkeditor.utils.StringHelper; +import com.reandroid.commons.command.ARGException; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public class InfoOptions extends Options { + public boolean verbose = true; + public boolean packageName = true; + public boolean versionCode = true; + public boolean versionName = true; + public boolean appName = true; + public boolean appIcon = true; + public boolean appRoundIcon = true; + public boolean permissions = true; + public boolean appClass = true; + public boolean activities = true; + public final List resList; + public boolean resources = false; + public final List typeFilterList; + public InfoOptions(){ + super(); + this.resList = new ArrayList<>(); + this.typeFilterList = new ArrayList<>(); + } + @Override + public void parse(String[] args) throws ARGException { + parseInput(args); + + verbose = containsArg(ARG_verbose, args); + type = parseType(ARG_type, args, availableTypes, TYPE_TEXT); + + parseOutput(args); + + initializeDefaults(args); + + parseResList(args); + + parseResourceFilterList(args); + + packageName = containsArg(ARG_package, args, packageName); + versionCode = containsArg(ARG_version_code, args, versionCode); + versionName = containsArg(ARG_version_name, args, versionName); + appName = containsArg(ARG_app_name, args, appName); + appIcon = containsArg(ARG_app_icon, args, appIcon); + appRoundIcon = containsArg(ARG_app_round_icon, args, appRoundIcon); + permissions = containsArg(ARG_permissions, args, permissions); + appClass = containsArg(ARG_app_class, args, appClass); + activities = containsArg(ARG_activities, args, activities); + resources = containsArg(ARG_resources, args, false); + + + + parseHelp(args); + + super.checkUnknownOptions(args); + } + private void parseOutput(String[] args) throws ARGException { + this.outputFile = null; + File file = parseFile(ARG_output, args); + if(file == null){ + return; + } + String name = file.getName().toLowerCase(); + String ext; + if(TYPE_TEXT.equals(type)){ + if(name.endsWith(".text")){ + ext = ".text"; + }else { + ext = ".txt"; + } + }else { + ext = "." + type.toLowerCase(); + } + if(!name.endsWith(ext)){ + throw new ARGException("Invalid file extension! Expected = \"" + + ext + "\", " + file); + } + this.outputFile = file; + } + private void parseResourceFilterList(String[] args) throws ARGException { + String filter; + while (( filter = parseArgValue(ARG_filter_type, args)) != null){ + typeFilterList.add(filter); + } + } + private void parseResList(String[] args) throws ARGException { + String res; + while (( res = parseArgValue(ARG_res, args)) != null){ + resList.add(res); + verbose = true; + } + } + private void initializeDefaults(String[] args){ + resources = false; + if(!Util.isEmpty(args)){ + packageName = false; + versionCode = false; + versionName = false; + appName = false; + appIcon = false; + appRoundIcon = false; + permissions = false; + activities = false; + appClass = false; + } + } + private void parseInput(String[] args) throws ARGException { + this.inputFile = null; + File file = parseFile(ARG_input, args); + if(file == null){ + throw new ARGException("Missing input apk! Specify with " + ARG_input); + } + if(!file.isFile()){ + throw new ARGException("No such file: "+file); + } + this.inputFile = file; + } + private void parseHelp(String[] args) throws ARGException { + if(!Util.containsHelp(args)){ + return; + } + throw new ARGException(getHelp()); + } + public static String getHelp(){ + StringBuilder builder=new StringBuilder(); + builder.append(Info.DESCRIPTION); + builder.append("\nOptions:\n"); + String[][] table=new String[][]{ + new String[]{ARG_input, ARG_DESC_input}, + new String[]{ARG_type, ARG_DESC_type}, + new String[]{ARG_output, ARG_DESC_output}, + new String[]{ARG_res, ARG_DESC_res}, + new String[]{ARG_filter_type, ARG_DESC_filter_type}, + }; + StringHelper.printTwoColumns(builder, " ", PRINT_WIDTH, table); + builder.append("\n\nFlags:\n"); + table=new String[][]{ + new String[]{ARG_verbose, ARG_DESC_verbose}, + new String[]{ARG_package, ARG_DESC_package}, + new String[]{ARG_version_code, ARG_DESC_version_code}, + new String[]{ARG_version_name, ARG_DESC_version_name}, + new String[]{ARG_app_name, ARG_DESC_app_name}, + new String[]{ARG_app_icon, ARG_DESC_app_icon}, + new String[]{ARG_app_round_icon, ARG_DESC_app_round_icon}, + new String[]{ARG_app_class, ARG_DESC_app_class}, + new String[]{ARG_permissions, ARG_DESC_permissions}, + new String[]{ARG_activities, ARG_DESC_activities}, + new String[]{ARG_resources, ARG_DESC_resources}, + new String[]{" ", " "}, + new String[]{ARG_ALL_help, ARG_DESC_help} + }; + StringHelper.printTwoColumns(builder, " ", PRINT_WIDTH, table); + + builder.append("\n").append(Options.LINE); + + String jar = APKEditor.getJarName(); + + builder.append("\n\nExample-1:"); + builder.append("\n java -jar ").append(jar).append(" ").append(Info.ARG_SHORT).append(" ") + .append(ARG_input).append(" file.apk"); + + builder.append("\n\nExample-2:"); + builder.append("\n java -jar ").append(jar).append(" ").append(Info.ARG_SHORT).append(" ") + .append(ARG_input).append(" file.apk"); + builder.append(" ").append(ARG_type).append(" ").append(TYPE_JSON.toUpperCase()); + builder.append(" ").append(ARG_verbose); + builder.append(" ").append(ARG_output).append(" info_file.json"); + + builder.append("\n\nExample-3:"); + builder.append("\n java -jar ").append(jar).append(" ").append(Info.ARG_SHORT).append(" ") + .append(ARG_input).append(" file.apk ") + .append(ARG_resources) + .append(" ").append(ARG_filter_type).append(" ").append("mipmap") + .append(" ").append(ARG_filter_type).append(" ").append("drawable"); + + builder.append("\n\nExample-4:"); + builder.append("\n java -jar ").append(jar).append(" ").append(Info.ARG_SHORT).append(" ") + .append(ARG_input).append(" file.apk ") + .append(ARG_verbose) + .append(" ").append(ARG_res).append(" ").append("@string/app_name") + .append(" ").append(ARG_res).append(" ").append("0x7f010000"); + + return builder.toString(); + } + + protected static final String ARG_output = "-o"; + protected static final String ARG_DESC_output = "Output path, default is print to std stream."; + + private static final String ARG_type = "-t"; + private static final String ARG_DESC_type = "Print type, options:\n 1) TEXT\n 2) JSON\n 3) XML\n default=TEXT"; + + private static final String ARG_verbose = "-v"; + private static final String ARG_DESC_verbose = "Verbose mode."; + + private static final String ARG_package = "-package"; + private static final String ARG_DESC_package = "Package name(s) from manifest and if verbose mode, prints resource table packages."; + + private static final String ARG_version_code = "-version-code"; + private static final String ARG_DESC_version_code = "App version code."; + private static final String ARG_version_name = "-version-name"; + private static final String ARG_DESC_version_name = "App version name."; + + private static final String ARG_app_name = "-app-name"; + private static final String ARG_DESC_app_name = "App name. If verbose mode, prints all configurations."; + private static final String ARG_app_icon = "-app-icon"; + private static final String ARG_DESC_app_icon = "App icon path/value. If verbose mode, prints all configurations."; + private static final String ARG_app_round_icon = "-app-round-icon"; + private static final String ARG_DESC_app_round_icon = "App round icon path/value. If verbose mode, prints all configurations."; + private static final String ARG_permissions = "-permissions"; + private static final String ARG_DESC_permissions = "Permissions."; + private static final String ARG_app_class = "-app-class"; + private static final String ARG_DESC_app_class = "Application class name."; + private static final String ARG_activities = "-activities"; + private static final String ARG_DESC_activities = "Prints main activity class name. If verbose mode, " + + "prints all declared activities including ."; + + private static final String ARG_res = "-res"; + private static final String ARG_DESC_res = "Prints resource entries specified by either of:" + + "\n 1) Hex or decimal resource id.\n 2) Full resource name e.g @string/app_name." + + "\n *Can be multiple"; + + + private static final String ARG_resources = "-resources"; + private static final String ARG_DESC_resources = "Prints all resources."; + + + private static final String ARG_filter_type = "-filter-type"; + private static final String ARG_DESC_filter_type = "Prints only the specified resource type names" + + "\n *This applies only when flag '-resources' used." + + "\n *Can be multiple"; + + + private static final String[] availableTypes = new String[]{TYPE_TEXT, TYPE_JSON, TYPE_XML}; +} diff --git a/src/main/java/com/reandroid/apkeditor/info/InfoWriter.java b/src/main/java/com/reandroid/apkeditor/info/InfoWriter.java new file mode 100644 index 00000000..4d0b9420 --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/info/InfoWriter.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.apkeditor.info; + +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.coder.ValueCoder; +import com.reandroid.arsc.container.SpecTypePair; +import com.reandroid.arsc.group.EntryGroup; +import com.reandroid.arsc.util.HexUtil; +import com.reandroid.arsc.value.*; + +import java.io.Closeable; +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; + +public abstract class InfoWriter implements Closeable { + private final Writer writer; + public InfoWriter(Writer writer){ + this.writer = writer; + } + + public void writeResources(PackageBlock packageBlock, List typeFilters, boolean writeEntries) throws IOException { + List entryGroupList = toSortedEntryGroups(packageBlock.listEntryGroup()); + for(EntryGroup entryGroup : entryGroupList){ + writeResources(entryGroup, writeEntries); + } + } + public abstract void writeResources(EntryGroup entryGroup, boolean writeEntries) throws IOException; + public abstract void writePackageNames(Collection packageBlocks) throws IOException; + public abstract void writeEntries(String name, List entryList) throws IOException; + public abstract void writeArray(String name, Object[] values) throws IOException; + public abstract void writeNameValue(String name, Object value) throws IOException; + public abstract void flush() throws IOException; + boolean contains(SpecTypePair specTypePair, List filterList){ + if(filterList.size() == 0){ + return true; + } + return filterList.contains(specTypePair.getTypeName()); + } + public Writer getWriter() { + return writer; + } + @Override + public void close() throws IOException{ + this.writer.close(); + } + + static String toString(Object obj){ + if(obj != null){ + return obj.toString(); + } + return null; + } + static String getValueAsString(Entry entry){ + ResValue resValue = entry.getResValue(); + if(resValue == null){ + return ""; + } + return getValueAsString(resValue); + } + static String getValueAsString(Value value){ + ValueType valueType = value.getValueType(); + if(valueType == ValueType.STRING){ + return value.getValueAsString(); + } + String decoded = ValueCoder.decode(valueType, value.getData()); + if(decoded != null){ + return decoded; + } + if(valueType == ValueType.ATTRIBUTE){ + return HexUtil.toHex8("?0x", value.getData()); + } + if(valueType == ValueType.REFERENCE){ + return HexUtil.toHex8("@0x", value.getData()); + } + return HexUtil.toHex8("0x", value.getData()); + } + + static List toSortedEntryGroups(Collection entryGroups){ + List results = new ArrayList<>(entryGroups); + sortEntryGroups(results); + return results; + } + static void sortEntryGroups(List entryGroups){ + Comparator cmp = new Comparator() { + @Override + public int compare(EntryGroup entryGroup1, EntryGroup entryGroup2) { + long l1 = 0x00000000ffffffffL & entryGroup1.getResourceId(); + long l2 = 0x00000000ffffffffL & entryGroup2.getResourceId(); + return Long.compare(l1, l2); + } + }; + entryGroups.sort(cmp); + } + + static List sortEntries(Collection entryCollection) { + ArrayList results; + if(entryCollection instanceof ArrayList){ + results = (ArrayList) entryCollection; + }else { + results = new ArrayList<>(entryCollection); + } + Comparator cmp = new Comparator() { + @Override + public int compare(Entry entry1, Entry entry2) { + return entry1.getResConfig().compareTo(entry2.getResConfig()); + } + }; + results.sort(cmp); + return results; + } + + static final String TAG_RES_PACKAGES = "resource-packages"; + static final String TAG_PUBLIC = "public"; + static final String TAG_RESOURCES = "resources"; + static final String NAME_RESOURCE = "resource"; + static final String TAG_CONFIG = "config"; + static final String TAG_VALUE = "value"; + static final String TAG_BAG = "bag"; + static final String TAG_ITEM = "item"; + static final String NAME_QUALIFIERS = "qualifiers"; + +} diff --git a/src/main/java/com/reandroid/apkeditor/info/InfoWriterJson.java b/src/main/java/com/reandroid/apkeditor/info/InfoWriterJson.java new file mode 100644 index 00000000..33160991 --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/info/InfoWriterJson.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.apkeditor.info; + +import com.reandroid.arsc.array.ResValueMapArray; +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.container.SpecTypePair; +import com.reandroid.arsc.group.EntryGroup; +import com.reandroid.arsc.value.Entry; +import com.reandroid.arsc.value.ResTableMapEntry; +import com.reandroid.arsc.value.ResValue; +import com.reandroid.arsc.value.ResValueMap; +import com.reandroid.json.JSONWriter; + +import java.io.IOException; +import java.io.Writer; +import java.util.Collection; +import java.util.List; + +public class InfoWriterJson extends InfoWriter{ + private final JSONWriter mJsonWriter; + public InfoWriterJson(Writer writer) { + super(writer); + JSONWriter jsonWriter = new JSONWriter(writer); + jsonWriter = jsonWriter.array(); + this.mJsonWriter = jsonWriter; + } + + @Override + public void writeResources(PackageBlock packageBlock, List typeFilters, boolean writeEntries) throws IOException { + packageBlock.sortTypes(); + JSONWriter jsonWriter = mJsonWriter.object() + .key("id").value(packageBlock.getId()) + .key("package").value(packageBlock.getName()) + .key("types").array(); + + for(SpecTypePair specTypePair : packageBlock.listSpecTypePairs()){ + if(!contains(specTypePair, typeFilters)){ + continue; + } + writeResources(specTypePair, writeEntries); + } + jsonWriter.endArray() + .endObject(); + } + public void writeResources(SpecTypePair specTypePair, boolean writeEntries) throws IOException { + JSONWriter jsonWriter = mJsonWriter.object() + .key("id").value(specTypePair.getId()) + .key("type").value(specTypePair.getTypeName()) + .key("entries").array(); + + List entryGroupList = toSortedEntryGroups( + specTypePair.createEntryGroups(true).values()); + + for(EntryGroup entryGroup : entryGroupList){ + writeResources(entryGroup, writeEntries); + } + jsonWriter.endArray().endObject(); + } + @Override + public void writeResources(EntryGroup entryGroup, boolean writeEntries) throws IOException { + JSONWriter jsonWriter = mJsonWriter.object() + .key("id").value(entryGroup.getResourceId()) + .key("type").value(entryGroup.getTypeName()) + .key("name").value(entryGroup.getSpecName()); + if(writeEntries){ + jsonWriter.key("configs"); + writeEntries(sortEntries(entryGroup.listItems())); + } + jsonWriter.endObject(); + } + + public void writeEntries(List entryList) throws IOException { + JSONWriter jsonWriter = mJsonWriter.array(); + for(Entry entry : entryList){ + writeEntry(entry); + } + jsonWriter.endArray(); + } + public void writeEntry(Entry entry) throws IOException { + if(entry.isComplex()){ + writeBagEntry(entry); + }else { + writeResEntry(entry); + } + } + private void writeResEntry(Entry entry) throws IOException { + ResValue resValue = entry.getResValue(); + if(resValue == null){ + return; + } + mJsonWriter.object() + .key(NAME_QUALIFIERS).value(entry.getResConfig().getQualifiers()) + .key("value").value(getValueAsString(resValue)) + .endObject(); + } + private void writeBagEntry(Entry entry) { + ResValueMapArray mapArray = entry.getResValueMapArray(); + JSONWriter jsonWriter = mJsonWriter.object() + .key(NAME_QUALIFIERS).value(entry.getResConfig().getQualifiers()) + .key("size").value(mapArray.childesCount()) + .key("parent").value(((ResTableMapEntry)entry.getTableEntry()).getParentId()) + .key(TAG_BAG).array(); + for(ResValueMap resValueMap : mapArray.getChildes()){ + writeValueMap(resValueMap); + } + jsonWriter.endArray() + .endObject(); + } + private void writeValueMap(ResValueMap resValueMap){ + mJsonWriter.object() + .key("name").value(resValueMap.decodeName()) + .key("id").value(resValueMap.getNameResourceID()) + .key("value").value(getValueAsString(resValueMap)) + .endObject(); + } + @Override + public void writePackageNames(Collection packageBlocks) throws IOException { + if(packageBlocks == null || packageBlocks.size() == 0){ + return; + } + JSONWriter jsonWriter = mJsonWriter.object() + .key(TAG_RES_PACKAGES).array(); + + for(PackageBlock packageBlock : packageBlocks){ + jsonWriter.object() + .key("id").value(packageBlock.getId()) + .key("name").value(packageBlock.getName()) + .endObject(); + } + jsonWriter.endArray() + .endObject(); + } + @Override + public void writeEntries(String name, List entryList) throws IOException { + if(entryList == null || entryList.size() == 0){ + return; + } + Entry first = entryList.get(0); + JSONWriter jsonWriter = mJsonWriter.object() + .key("id").value(first.getResourceId()) + .key("type").value(first.getTypeName()) + .key("name").value(first.getName()) + .key("entries") + .array(); + + for(Entry entry : entryList){ + jsonWriter.object() + .key("config").value(entry.getResConfig().getQualifiers()) + .key("value").value(getValueAsString(entry)) + .endObject(); + } + jsonWriter.endArray() + .endObject(); + } + @Override + public void writeArray(String name, Object[] values) throws IOException { + + JSONWriter jsonWriter = mJsonWriter.object() + .key(name) + .array(); + + for(Object value:values){ + jsonWriter.value(value); + } + + jsonWriter.endArray() + .endObject(); + } + @Override + public void writeNameValue(String name, Object value) throws IOException { + mJsonWriter.object() + .key(name) + .value(value) + .endObject(); + getWriter().flush(); + } + @Override + public void flush() throws IOException { + Writer writer = getWriter(); + mJsonWriter.endArray(); + writer.write("\n"); + writer.flush(); + } +} diff --git a/src/main/java/com/reandroid/apkeditor/info/InfoWriterText.java b/src/main/java/com/reandroid/apkeditor/info/InfoWriterText.java new file mode 100644 index 00000000..3cbbe186 --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/info/InfoWriterText.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.apkeditor.info; + +import com.reandroid.arsc.array.ResValueMapArray; +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.container.SpecTypePair; +import com.reandroid.arsc.group.EntryGroup; +import com.reandroid.arsc.util.HexUtil; +import com.reandroid.arsc.value.Entry; +import com.reandroid.arsc.value.ResTableMapEntry; +import com.reandroid.arsc.value.ResValue; +import com.reandroid.arsc.value.ResValueMap; + +import java.io.IOException; +import java.io.Writer; +import java.util.Collection; +import java.util.List; + +public class InfoWriterText extends InfoWriter{ + public InfoWriterText(Writer writer) { + super(writer); + } + + @Override + public void writeResources(PackageBlock packageBlock, List typeFilters, boolean writeEntries) throws IOException { + Writer writer = getWriter(); + writer.write("Package name="); + writer.write(packageBlock.getName()); + writer.write(" id="); + writer.write(HexUtil.toHex2((byte) packageBlock.getId())); + writer.write("\n"); + packageBlock.sortTypes(); + + for(SpecTypePair specTypePair : packageBlock.listSpecTypePairs()){ + if(!contains(specTypePair, typeFilters)){ + continue; + } + writeResources(specTypePair, writeEntries); + } + } + public void writeResources(SpecTypePair specTypePair, boolean writeEntries) throws IOException { + Writer writer = getWriter(); + writer.write(" type "); + writer.write(specTypePair.getTypeName()); + writer.write(" id="); + writer.write(Integer.toString(specTypePair.getId())); + writer.write(" entryCount="); + writer.write(Integer.toString(specTypePair.getHighestEntryCount())); + writer.write("\n"); + List entryGroupList = toSortedEntryGroups( + specTypePair.createEntryGroups(true).values()); + + for(EntryGroup entryGroup : entryGroupList){ + writeResources(entryGroup, writeEntries); + } + } + @Override + public void writeResources(EntryGroup entryGroup, boolean writeEntries) throws IOException { + Writer writer = getWriter(); + writer.write(" "); + writer.write(NAME_RESOURCE); + writer.write(" "); + writer.write(HexUtil.toHex8(entryGroup.getResourceId())); + writer.write(" "); + writer.write(entryGroup.getTypeName()); + writer.write("/"); + writer.write(entryGroup.getSpecName()); + writer.write("\n"); + if(writeEntries){ + writeEntries(sortEntries(entryGroup.listItems())); + } + writer.flush(); + } + + public void writeEntries(List entryList) throws IOException { + for(Entry entry : entryList){ + writeEntry(entry); + } + } + public void writeEntry(Entry entry) throws IOException { + Writer writer = getWriter(); + writer.write(" ("); + writer.write(entry.getResConfig().getQualifiers()); + writer.write(") "); + //write file + if(entry.isComplex()){ + writeBagEntry(entry); + }else { + writeResEntry(entry); + } + writer.flush(); + } + private void writeResEntry(Entry entry) throws IOException { + ResValue resValue = entry.getResValue(); + if(resValue == null){ + return; + } + Writer writer = getWriter(); + writer.write(getValueAsString(resValue)); + writer.write("\n"); + } + private void writeBagEntry(Entry entry) throws IOException { + Writer writer = getWriter(); + ResValueMapArray mapArray = entry.getResValueMapArray(); + writer.write(" size="); + writer.write(Integer.toString(mapArray.childesCount())); + writer.write(" parent="); + writer.write(HexUtil.toHex8(((ResTableMapEntry)entry.getTableEntry()).getParentId())); + writer.write("\n"); + for(ResValueMap resValueMap : mapArray.getChildes()){ + writeValueMap(resValueMap); + } + } + private void writeValueMap(ResValueMap resValueMap) throws IOException { + Writer writer = getWriter(); + writer.write(" "); + String name = resValueMap.decodeName(); + if(name != null){ + writer.write(name); + writer.write("("); + } + writer.write(HexUtil.toHex8(resValueMap.getNameResourceID())); + if(name != null){ + writer.write(")"); + } + writer.write("="); + writer.write(getValueAsString(resValueMap)); + writer.write("\n"); + } + @Override + public void writePackageNames(Collection packageBlocks) throws IOException { + if(packageBlocks == null || packageBlocks.size() == 0){ + return; + } + Writer writer = getWriter(); + writer.write(TAG_RES_PACKAGES); + writer.write(" [ count "); + writer.write(Integer.toString(packageBlocks.size())); + writer.write("]"); + writer.write("\n"); + for(PackageBlock packageBlock : packageBlocks){ + writer.write(ARRAY_TAB); + writer.write(HexUtil.toHex2((byte) packageBlock.getId())); + writer.write(" \""); + writer.write(packageBlock.getName()); + writer.write("\""); + writer.write("\n"); + } + } + @Override + public void writeEntries(String name, List entryList) throws IOException { + if(entryList == null || entryList.size() == 0){ + return; + } + Entry first = entryList.get(0); + Writer writer = getWriter(); + writer.write(name); + writer.write(" [ configs = "); + writer.write(Integer.toString(entryList.size())); + writer.write(", id="); + writer.write(HexUtil.toHex8(first.getResourceId())); + writer.write(", type="); + writer.write(first.getTypeName()); + writer.write(", name="); + writer.write(first.getName()); + writer.write(" ]"); + writer.write("\n"); + int index = 0; + for(Entry entry : entryList){ + index++; + String config = entry.getResConfig().getQualifiers(); + if(config.length() == 0){ + config = "default"; + } + writer.write(ARRAY_TAB); + writer.write(config); + writer.write(" \""); + String text = getValueAsString(entry); + writer.write(text); + writer.write("\"\n"); + if((index % 3) == 0){ + writer.flush(); + } + } + } + + @Override + public void writeArray(String name, Object[] values) throws IOException { + if(values == null){ + return; + } + Writer writer = getWriter(); + writer.write(name); + writer.write(" [ count "); + writer.write(Integer.toString(values.length)); + writer.write("]"); + writer.write("\n"); + String format = "%0" + getDecimalPlaces(values.length) + "d) "; + int index = 0; + for(Object value : values){ + index ++; + writer.write(ARRAY_TAB); + writer.write(String.format(format, index)); + String text = toString(value); + if(text == null){ + text = "null"; + } + writer.write(text); + writer.write("\n"); + if((index % 3) == 0){ + writer.flush(); + } + } + } + @Override + public void writeNameValue(String name, Object value) throws IOException { + String text = toString(value); + if(text == null){ + return; + } + Writer writer = getWriter(); + writer.write(name); + writer.write("=\""); + writer.write(text); + writer.write("\""); + writer.write("\n"); + writer.flush(); + } + + @Override + public void flush() throws IOException { + Writer writer = getWriter(); + writer.flush(); + } + + private static int getDecimalPlaces(int max){ + int i = 0; + while (max != 0){ + i++; + max = max / 10; + } + return i; + } + + private static final String ARRAY_TAB = " "; +} diff --git a/src/main/java/com/reandroid/apkeditor/info/InfoWriterXml.java b/src/main/java/com/reandroid/apkeditor/info/InfoWriterXml.java new file mode 100644 index 00000000..dde8eac1 --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/info/InfoWriterXml.java @@ -0,0 +1,325 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.apkeditor.info; + +import com.android.org.kxml2.io.KXmlSerializer; +import com.reandroid.arsc.array.ResValueMapArray; +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.container.SpecTypePair; +import com.reandroid.arsc.group.EntryGroup; +import com.reandroid.arsc.util.HexUtil; +import com.reandroid.arsc.value.Entry; +import com.reandroid.arsc.value.ResTableMapEntry; +import com.reandroid.arsc.value.ResValue; +import com.reandroid.arsc.value.ResValueMap; + +import java.io.IOException; +import java.io.Writer; +import java.util.Collection; +import java.util.List; + +public class InfoWriterXml extends InfoWriter{ + private KXmlSerializer mSerializer; + private String mRootTag = TAG_INFO; + private int mIndent; + public InfoWriterXml(Writer writer) { + super(writer); + } + @Override + public void writeResources(PackageBlock packageBlock, List typeFilters, boolean writeEntries) throws IOException { + KXmlSerializer serializer = getSerializer(); + int indent = mIndent + 2; + mIndent = indent; + writeIndent(serializer, indent); + serializer.startTag(null, "package"); + serializer.attribute(null, "id", HexUtil.toHex2((byte) packageBlock.getId())); + serializer.attribute(null, "name", packageBlock.getName()); + packageBlock.sortTypes(); + + for(SpecTypePair specTypePair : packageBlock.listSpecTypePairs()){ + if(!contains(specTypePair, typeFilters)){ + continue; + } + writeResources(specTypePair, writeEntries); + } + + writeIndent(serializer, indent); + indent = indent - 2; + mIndent = indent; + serializer.endTag(null, "package"); + } + public void writeResources(SpecTypePair specTypePair, boolean writeEntries) throws IOException { + KXmlSerializer serializer = getSerializer(); + int indent = mIndent + 2; + mIndent = indent; + writeIndent(serializer, indent); + serializer.startTag(null, "type"); + + serializer.attribute(null, "name", + specTypePair.getTypeName()); + serializer.attribute(null, "id", + Integer.toString(specTypePair.getId())); + serializer.attribute(null, "entryCount", + Integer.toString(specTypePair.getHighestEntryCount())); + List entryGroupList = toSortedEntryGroups( + specTypePair.createEntryGroups(true).values()); + + for(EntryGroup entryGroup : entryGroupList){ + writeResources(entryGroup, writeEntries); + } + + writeIndent(serializer, indent); + indent = indent - 2; + mIndent = indent; + serializer.endTag(null, "type"); + serializer.flush(); + } + @Override + public void writeResources(EntryGroup entryGroup, boolean writeEntries) throws IOException { + KXmlSerializer serializer = getSerializer(); + int indent = mIndent + 2; + mIndent = indent; + writeIndent(serializer, indent); + serializer.startTag(null, NAME_RESOURCE); + serializer.attribute(null, "id", HexUtil.toHex8(entryGroup.getResourceId())); + serializer.attribute(null, "type", entryGroup.getTypeName()); + serializer.attribute(null, "name", entryGroup.getSpecName()); + + if(writeEntries){ + + writeEntries(sortEntries(entryGroup.listItems())); + + writeIndent(serializer, indent); + } + + indent = indent - 2; + mIndent = indent; + + serializer.endTag(null, NAME_RESOURCE); + serializer.flush(); + } + public void writeEntries(List entryList) throws IOException { + for(Entry entry : entryList){ + writeEntry(entry); + } + } + public void writeEntry(Entry entry) throws IOException { + KXmlSerializer serializer = getSerializer(); + int indent = mIndent + 2; + mIndent = indent; + writeIndent(serializer, indent); + serializer.startTag(null, TAG_CONFIG); + serializer.attribute(null, NAME_QUALIFIERS, + entry.getResConfig().getQualifiers()); + if(entry.isComplex()){ + writeBagEntry(entry); + }else { + writeResEntry(entry); + } + writeIndent(serializer, indent); + serializer.endTag(null, TAG_CONFIG); + indent = indent - 2; + mIndent = indent; + } + private void writeResEntry(Entry entry) throws IOException { + ResValue resValue = entry.getResValue(); + if(resValue == null){ + return; + } + KXmlSerializer serializer = getSerializer(); + int indent = mIndent + 2; + mIndent = indent; + writeIndent(serializer, indent); + serializer.startTag(null, TAG_VALUE); + serializer.attribute(null, "type", resValue.getValueType().name()); + serializer.text(getValueAsString(resValue)); + serializer.endTag(null, TAG_VALUE); + indent = indent - 2; + mIndent = indent; + } + private void writeBagEntry(Entry entry) throws IOException { + KXmlSerializer serializer = getSerializer(); + int indent = mIndent + 2; + mIndent = indent; + writeIndent(serializer, indent); + serializer.startTag(null, TAG_BAG); + serializer.attribute(null, "parent", + HexUtil.toHex8(((ResTableMapEntry)entry.getTableEntry()).getParentId())); + ResValueMapArray mapArray = entry.getResValueMapArray(); + for(ResValueMap resValueMap : mapArray.getChildes()){ + writeValueMap(resValueMap); + } + writeIndent(serializer, indent); + serializer.endTag(null, TAG_BAG); + indent = indent - 2; + mIndent = indent; + } + private void writeValueMap(ResValueMap resValueMap) throws IOException { + KXmlSerializer serializer = getSerializer(); + int indent = mIndent + 2; + mIndent = indent; + writeIndent(serializer, indent); + serializer.startTag(null, TAG_VALUE); + serializer.attribute(null, "name", + HexUtil.toHex8(resValueMap.getNameResourceID())); + serializer.attribute(null, "type", resValueMap.getValueType().name()); + serializer.text(getValueAsString(resValueMap)); + serializer.endTag(null, TAG_VALUE); + indent = indent - 2; + mIndent = indent; + } + @Override + public void writePackageNames(Collection packageBlocks) throws IOException { + if(packageBlocks == null || packageBlocks.size() == 0){ + return; + } + int level = INDENT; + KXmlSerializer serializer = getSerializer(); + writeIndent(serializer, level); + serializer.startTag(null, TAG_RES_PACKAGES); + serializer.attribute(null, "count", Integer.toString(packageBlocks.size())); + level = level + 2; + for(PackageBlock packageBlock : packageBlocks){ + writeIndent(serializer, level); + serializer.startTag(null, "package"); + serializer.attribute(null, "id", + HexUtil.toHex2((byte) packageBlock.getId())); + serializer.attribute(null, "name", + packageBlock.getName()); + serializer.endTag(null, "package"); + } + level = level - 2; + writeIndent(serializer, level); + serializer.endTag(null, TAG_RES_PACKAGES); + serializer.flush(); + } + @Override + public void writeEntries(String name, List entryList) throws IOException { + if(entryList == null || entryList.size() == 0){ + return; + } + Entry first = entryList.get(0); + int level = INDENT; + KXmlSerializer serializer = getSerializer(); + writeIndent(serializer, level); + serializer.startTag(null, name); + serializer.attribute(null, "count", Integer.toString(entryList.size())); + serializer.attribute(null, "id", HexUtil.toHex8(first.getResourceId())); + serializer.attribute(null, "type", first.getTypeName()); + serializer.attribute(null, "name", first.getName()); + level = level + 2; + for(Entry entry : entryList){ + String config = entry.getResConfig().getQualifiers(); + if(config.length() == 0){ + config = "default"; + } + String text = getValueAsString(entry); + writeIndent(serializer, level); + serializer.startTag(null, "item"); + serializer.attribute(null, "config", config); + serializer.text(text); + serializer.endTag(null, "item"); + } + level = level - 2; + writeIndent(serializer, level); + serializer.endTag(null, name); + serializer.flush(); + } + + @Override + public void writeArray(String name, Object[] values) throws IOException { + if(values == null){ + return; + } + int level = INDENT; + KXmlSerializer serializer = getSerializer(); + writeIndent(serializer, level); + serializer.startTag(null, name); + serializer.attribute(null, "count", Integer.toString(values.length)); + level = level + 2; + for(Object value : values){ + String text = toString(value); + if(text == null){ + text = ""; + } + writeIndent(serializer, level); + serializer.startTag(null, "item"); + serializer.text(text); + serializer.endTag(null, "item"); + } + level = level - 2; + writeIndent(serializer, level); + serializer.endTag(null, name); + serializer.flush(); + } + + @Override + public void writeNameValue(String name, Object value) throws IOException { + String text = toString(value); + if(text == null){ + return; + } + KXmlSerializer serializer = getSerializer(); + writeIndent(serializer, INDENT); + serializer.startTag(null, name); + serializer.text(text); + serializer.endTag(null, name); + serializer.flush(); + } + + @Override + public void flush() throws IOException { + KXmlSerializer serializer = this.mSerializer; + if(serializer != null){ + writeIndent(serializer, 0); + serializer.endTag(null, mRootTag); + serializer.endDocument(); + writeIndent(serializer, 0); + serializer.flush(); + } + } + private void writeIndent(KXmlSerializer serializer, int level) throws IOException { + StringBuilder builder = new StringBuilder(); + builder.append('\n'); + for(int i = 0; i < level; i++){ + builder.append(' '); + } + serializer.text(builder.toString()); + } + private void setRootTag(String tag){ + KXmlSerializer serializer = this.mSerializer; + if(serializer != null){ + return; + } + mRootTag = tag; + } + private KXmlSerializer getSerializer() throws IOException { + KXmlSerializer serializer = this.mSerializer; + if(serializer != null){ + return serializer; + } + serializer = new KXmlSerializer(); + serializer.setOutput(getWriter()); + serializer.startDocument("utf-8", null); + writeIndent(serializer, 0); + serializer.startTag(null, mRootTag); + mSerializer = serializer; + return serializer; + } + + private static final int INDENT = 3; + private static final String TAG_INFO = "info"; +} diff --git a/src/main/java/com/reandroid/apkeditor/merge/Merger.java b/src/main/java/com/reandroid/apkeditor/merge/Merger.java index 7d245630..4cc386f7 100644 --- a/src/main/java/com/reandroid/apkeditor/merge/Merger.java +++ b/src/main/java/com/reandroid/apkeditor/merge/Merger.java @@ -1,48 +1,48 @@ - /* - * Copyright (C) 2022 github.com/REAndroid - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.reandroid.apkeditor.merge; - import com.reandroid.apkeditor.BaseCommand; - import com.reandroid.apkeditor.Util; - import com.reandroid.apkeditor.common.AndroidManifestHelper; - import com.reandroid.archive.APKArchive; - import com.reandroid.archive.WriteProgress; - import com.reandroid.archive2.Archive; - import com.reandroid.arsc.value.ResTableEntry; - import com.reandroid.arsc.value.ResValue; - import com.reandroid.commons.command.ARGException; - import com.reandroid.commons.utils.log.Logger; - import com.reandroid.apk.APKLogger; - import com.reandroid.apk.ApkBundle; - import com.reandroid.apk.ApkModule; - import com.reandroid.arsc.chunk.TableBlock; - import com.reandroid.arsc.chunk.xml.AndroidManifestBlock; - import com.reandroid.arsc.chunk.xml.ResXmlAttribute; - import com.reandroid.arsc.chunk.xml.ResXmlElement; - import com.reandroid.arsc.group.EntryGroup; - import com.reandroid.arsc.value.Entry; - import com.reandroid.arsc.value.ValueType; +import com.reandroid.apkeditor.BaseCommand; +import com.reandroid.apkeditor.Util; +import com.reandroid.apkeditor.common.AndroidManifestHelper; +import com.reandroid.archive.APKArchive; +import com.reandroid.archive.WriteProgress; +import com.reandroid.archive2.Archive; +import com.reandroid.arsc.container.SpecTypePair; +import com.reandroid.arsc.value.ResValue; +import com.reandroid.commons.command.ARGException; +import com.reandroid.commons.utils.log.Logger; +import com.reandroid.apk.APKLogger; +import com.reandroid.apk.ApkBundle; +import com.reandroid.apk.ApkModule; +import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.arsc.chunk.xml.AndroidManifestBlock; +import com.reandroid.arsc.chunk.xml.ResXmlAttribute; +import com.reandroid.arsc.chunk.xml.ResXmlElement; +import com.reandroid.arsc.group.EntryGroup; +import com.reandroid.arsc.value.Entry; +import com.reandroid.arsc.value.ValueType; - import java.io.File; - import java.io.IOException; - import java.util.List; - import java.util.Objects; - import java.util.zip.ZipEntry; +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import java.util.zip.ZipEntry; - public class Merger extends BaseCommand implements WriteProgress { +public class Merger extends BaseCommand implements WriteProgress { private final MergerOptions options; private APKLogger mApkLogger; public Merger(MergerOptions options){ @@ -79,13 +79,19 @@ public void run() throws IOException { } if(options.cleanMeta){ log("Clearing META-INF ..."); - removeSignature(mergedModule); - mergedModule.setApkSignatureBlock(null); - } - if(mergedModule.hasAndroidManifestBlock()){ - sanitizeManifest(mergedModule); + clearMeta(mergedModule); } + sanitizeManifest(mergedModule); Util.addApkEditorInfo(mergedModule, getClass().getSimpleName()); + String message = mergedModule.refreshTable(); + if(message != null){ + log(message); + } + message = mergedModule.refreshManifest(); + if(message != null){ + log(message); + } + log("Writing apk ..."); mergedModule.writeApk(options.outputFile, this); if(extracted){ @@ -119,9 +125,11 @@ private File toTmpDir(File file){ } return new File(dir, name); } - private void sanitizeManifest(ApkModule apkModule) throws IOException { - AndroidManifestBlock manifest=apkModule.getAndroidManifestBlock(); - + private void sanitizeManifest(ApkModule apkModule) { + if(!apkModule.hasAndroidManifestBlock()){ + return; + } + AndroidManifestBlock manifest = apkModule.getAndroidManifestBlock(); log("Sanitizing manifest ..."); boolean removed = AndroidManifestHelper.removeApplicationAttribute(manifest, AndroidManifestBlock.ID_extractNativeLibs); @@ -145,9 +153,9 @@ private void sanitizeManifest(ApkModule apkModule) throws IOException { } manifest.refresh(); } - private boolean removeSplitsTableEntry(ResXmlElement metaElement, ApkModule apkModule) throws IOException { + private boolean removeSplitsTableEntry(ResXmlElement metaElement, ApkModule apkModule) { ResXmlAttribute nameAttribute = metaElement.searchAttributeByResourceId(AndroidManifestBlock.ID_name); - if(nameAttribute.getValueType()!= ValueType.STRING){ + if(nameAttribute == null){ return false; } if(!"com.android.vending.splits".equals(nameAttribute.getValueAsString())){ @@ -159,30 +167,39 @@ private boolean removeSplitsTableEntry(ResXmlElement metaElement, ApkModule apkM valueAttribute=metaElement.searchAttributeByResourceId( AndroidManifestBlock.ID_resource); } - if(valueAttribute==null || valueAttribute.getValueType()!=ValueType.REFERENCE){ + if(valueAttribute == null + || valueAttribute.getValueType() != ValueType.REFERENCE){ + return false; + } + if(!apkModule.hasTableBlock()){ return false; } - TableBlock tableBlock=apkModule.getTableBlock(); + TableBlock tableBlock = apkModule.getTableBlock(); EntryGroup entryGroup = tableBlock.search(valueAttribute.getData()); - if(entryGroup==null){ + if(entryGroup == null){ return false; } - APKArchive apkArchive=apkModule.getApkArchive(); + APKArchive apkArchive = apkModule.getApkArchive(); List entryList = entryGroup.listItems(); - for(Entry entryBlock:entryList){ - if(entryBlock==null){ + for(Entry entry : entryList){ + if(entry == null){ + continue; + } + ResValue resValue = entry.getResValue(); + if(resValue == null){ continue; } - ResValue resValue = ((ResTableEntry)entryBlock.getTableEntry()).getValue(); String path = resValue.getValueAsString(); log("Removed from table: "+path); //Remove file entry apkArchive.remove(path); // It's not safe to destroy entry, resource id might be used in dex code. - // Better replace it with boolean value - entryBlock.setNull(true); + // Better replace it with boolean value. + entry.setNull(true); + SpecTypePair specTypePair = entry.getTypeBlock() + .getParentSpecTypePair(); + specTypePair.removeNullEntries(entry.getId()); } - tableBlock.refresh(); return true; } @Override @@ -264,5 +281,5 @@ public static boolean isCommand(String command){ } public static final String ARG_SHORT="m"; public static final String ARG_LONG="merge"; - public static final String DESCRIPTION="Merges split apk files from directory or XAPK, APKM, APKS ..."; + public static final String DESCRIPTION="Merges split apk files from directory or compressed apk files like XAPK, APKM, APKS ..."; } diff --git a/src/main/java/com/reandroid/apkeditor/merge/MergerOptions.java b/src/main/java/com/reandroid/apkeditor/merge/MergerOptions.java index 58d7d9ef..ac96dbc8 100644 --- a/src/main/java/com/reandroid/apkeditor/merge/MergerOptions.java +++ b/src/main/java/com/reandroid/apkeditor/merge/MergerOptions.java @@ -108,13 +108,13 @@ public static String getHelp(){ new String[]{ARG_output, ARG_DESC_output}, new String[]{ARG_resDir, ARG_DESC_resDir} }; - StringHelper.printTwoColumns(builder, " ", 75, table); + StringHelper.printTwoColumns(builder, " ", Options.PRINT_WIDTH, table); builder.append("\nFlags:\n"); table=new String[][]{ new String[]{ARG_force, ARG_DESC_force}, new String[]{ARG_cleanMeta, ARG_DESC_cleanMeta} }; - StringHelper.printTwoColumns(builder, " ", 75, table); + StringHelper.printTwoColumns(builder, " ", Options.PRINT_WIDTH, table); String jar = APKEditor.getJarName(); builder.append("\n\nExample-1:"); builder.append("\n java -jar ").append(jar).append(" ").append(Merger.ARG_SHORT).append(" ") diff --git a/src/main/java/com/reandroid/apkeditor/protect/ProtectorOptions.java b/src/main/java/com/reandroid/apkeditor/protect/ProtectorOptions.java index 36935cc9..760e6400 100644 --- a/src/main/java/com/reandroid/apkeditor/protect/ProtectorOptions.java +++ b/src/main/java/com/reandroid/apkeditor/protect/ProtectorOptions.java @@ -85,13 +85,13 @@ public static String getHelp(){ new String[]{ARG_input, ARG_DESC_input}, new String[]{ARG_output, ARG_DESC_output} }; - StringHelper.printTwoColumns(builder, " ", 75, table); + StringHelper.printTwoColumns(builder, " ", Options.PRINT_WIDTH, table); builder.append("\nFlags:\n"); table=new String[][]{ new String[]{ARG_force, ARG_DESC_force}, new String[]{ARG_skipManifest, ARG_DESC_skipManifest} }; - StringHelper.printTwoColumns(builder, " ", 75, table); + StringHelper.printTwoColumns(builder, " ", Options.PRINT_WIDTH, table); String jar = APKEditor.getJarName(); builder.append("\n\nExample-1:"); builder.append("\n java -jar ").append(jar).append(" ").append(Protector.ARG_SHORT).append(" ") diff --git a/src/main/java/com/reandroid/apkeditor/refactor/Refactor.java b/src/main/java/com/reandroid/apkeditor/refactor/Refactor.java index ecf77365..537aa165 100644 --- a/src/main/java/com/reandroid/apkeditor/refactor/Refactor.java +++ b/src/main/java/com/reandroid/apkeditor/refactor/Refactor.java @@ -1,147 +1,151 @@ - /* - * Copyright (C) 2022 github.com/REAndroid - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.reandroid.apkeditor.refactor; - import com.reandroid.apkeditor.BaseCommand; - import com.reandroid.apkeditor.Util; - import com.reandroid.archive.WriteProgress; - import com.reandroid.commons.command.ARGException; - import com.reandroid.commons.utils.log.Logger; - import com.reandroid.apk.APKLogger; - import com.reandroid.apk.ApkModule; +import com.reandroid.apkeditor.BaseCommand; +import com.reandroid.apkeditor.Util; +import com.reandroid.archive.WriteProgress; +import com.reandroid.commons.command.ARGException; +import com.reandroid.commons.utils.log.Logger; +import com.reandroid.apk.APKLogger; +import com.reandroid.apk.ApkModule; - import java.io.File; - import java.io.IOException; +import java.io.File; +import java.io.IOException; - public class Refactor extends BaseCommand implements WriteProgress { - private final RefactorOptions options; - private APKLogger mApkLogger; - public Refactor(RefactorOptions options){ - this.options=options; - } - public void run() throws IOException { - log("Loading apk: "+options.inputFile); - ApkModule module=ApkModule.loadApkFile(options.inputFile); - module.setAPKLogger(getAPKLogger()); - if(!module.hasTableBlock()){ - throw new IOException("Don't have resources.arsc"); - } - String protect = Util.isProtected(module); - if(protect!=null){ - log(options.inputFile.getAbsolutePath()); - log(protect); - return; - } - if(options.fixTypeNames){ - TypeNameRefactor typeNameRefactor=new TypeNameRefactor(module); - typeNameRefactor.setApkLogger(getAPKLogger()); - typeNameRefactor.refactor(); - } - log("Auto refactoring ..."); - AutoRefactor autoRefactor=new AutoRefactor(module); - int autoRenameCount=autoRefactor.refactor(); - log("Auto renamed entries: "+autoRenameCount); - StringValueNameGenerator generator = new StringValueNameGenerator(module.getTableBlock()); - generator.refactor(); - if(options.publicXml!=null){ - log("Renaming from: "+options.publicXml); - PublicXmlRefactor publicXmlRefactor = - new PublicXmlRefactor(module, options.publicXml); - int pubXmlRenameCount = publicXmlRefactor.refactor(); - log("Renamed from public.xml entries: "+pubXmlRenameCount); - } - if(options.cleanMeta){ - log("Clearing META-INF ..."); - removeSignature(module); - } - Util.addApkEditorInfo(module, getClass().getSimpleName()); - log("Writing apk ..."); - module.writeApk(options.outputFile, this); - log("Saved to: "+options.outputFile); - log("Done"); - } - @Override - public void onCompressFile(String path, int method, long length) { - StringBuilder builder=new StringBuilder(); - builder.append("Writing: "); - if(path.length()>30){ - path=path.substring(path.length()-30); - } - builder.append(path); - logSameLine(builder.toString()); - } - private APKLogger getAPKLogger(){ - if(mApkLogger!=null){ - return mApkLogger; - } - mApkLogger = new APKLogger() { - @Override - public void logMessage(String msg) { - Logger.i(getLogTag()+msg); - } - @Override - public void logError(String msg, Throwable tr) { - Logger.e(getLogTag()+msg, tr); - } - @Override - public void logVerbose(String msg) { - if(msg.length()>30){ - msg=msg.substring(msg.length()-30); - } - Logger.sameLine(getLogTag()+msg); - } - }; - return mApkLogger; - } - public static void execute(String[] args) throws ARGException, IOException { - if(Util.isHelp(args)){ - throw new ARGException(RefactorOptions.getHelp()); - } - RefactorOptions option=new RefactorOptions(); - option.parse(args); - File outFile=option.outputFile; - Util.deleteEmptyDirectories(outFile); - if(outFile.exists()){ - if(!option.force){ - throw new ARGException("Path already exists: "+outFile); - } - log("Deleting: "+outFile); - Util.deleteDir(outFile); - } - log("Refactoring ...\n"+option); - Refactor refactor=new Refactor(option); - refactor.run(); - } - private static void logSameLine(String msg){ - Logger.sameLine(getLogTag()+msg); - } - private static void log(String msg){ - Logger.i(getLogTag()+msg); - } - private static String getLogTag(){ - return "[REFACTOR] "; - } - public static boolean isCommand(String command){ - if(Util.isEmpty(command)){ - return false; - } - command=command.toLowerCase().trim(); - return command.equals(ARG_SHORT) || command.equals(ARG_LONG); - } - public static final String ARG_SHORT="x"; - public static final String ARG_LONG="refactor"; - public static final String DESCRIPTION="Refactors obfuscated resource names"; +public class Refactor extends BaseCommand implements WriteProgress { + private final RefactorOptions options; + private APKLogger mApkLogger; + public Refactor(RefactorOptions options){ + this.options=options; + } + public void run() throws IOException { + log("Loading apk: "+options.inputFile); + ApkModule module=ApkModule.loadApkFile(options.inputFile); + module.setAPKLogger(getAPKLogger()); + if(!module.hasTableBlock()){ + throw new IOException("Don't have resources.arsc"); + } + String protect = Util.isProtected(module); + if(protect!=null){ + log(options.inputFile.getAbsolutePath()); + log(protect); + return; + } + if(options.fixTypeNames){ + TypeNameRefactor typeNameRefactor=new TypeNameRefactor(module); + typeNameRefactor.setApkLogger(getAPKLogger()); + typeNameRefactor.refactor(); + } + log("Auto refactoring ..."); + AutoRefactor autoRefactor=new AutoRefactor(module); + int autoRenameCount=autoRefactor.refactor(); + log("Auto renamed entries: "+autoRenameCount); + StringValueNameGenerator generator = new StringValueNameGenerator(module.getTableBlock()); + generator.refactor(); + if(options.publicXml!=null){ + log("Renaming from: "+options.publicXml); + PublicXmlRefactor publicXmlRefactor = + new PublicXmlRefactor(module, options.publicXml); + int pubXmlRenameCount = publicXmlRefactor.refactor(); + log("Renamed from public.xml entries: "+pubXmlRenameCount); + } + if(options.cleanMeta){ + log("Clearing META-INF ..."); + clearMeta(module); + } + Util.addApkEditorInfo(module, getClass().getSimpleName()); + String message = module.refreshTable(); + if(message != null){ + log(message); + } + log("Writing apk ..."); + module.writeApk(options.outputFile, this); + log("Saved to: "+options.outputFile); + log("Done"); + } + @Override + public void onCompressFile(String path, int method, long length) { + StringBuilder builder=new StringBuilder(); + builder.append("Writing: "); + if(path.length()>30){ + path=path.substring(path.length()-30); + } + builder.append(path); + logSameLine(builder.toString()); + } + private APKLogger getAPKLogger(){ + if(mApkLogger!=null){ + return mApkLogger; + } + mApkLogger = new APKLogger() { + @Override + public void logMessage(String msg) { + Logger.i(getLogTag()+msg); + } + @Override + public void logError(String msg, Throwable tr) { + Logger.e(getLogTag()+msg, tr); + } + @Override + public void logVerbose(String msg) { + if(msg.length()>30){ + msg=msg.substring(msg.length()-30); + } + Logger.sameLine(getLogTag()+msg); + } + }; + return mApkLogger; + } + public static void execute(String[] args) throws ARGException, IOException { + if(Util.isHelp(args)){ + throw new ARGException(RefactorOptions.getHelp()); + } + RefactorOptions option=new RefactorOptions(); + option.parse(args); + File outFile=option.outputFile; + Util.deleteEmptyDirectories(outFile); + if(outFile.exists()){ + if(!option.force){ + throw new ARGException("Path already exists: "+outFile); + } + log("Deleting: "+outFile); + Util.deleteDir(outFile); + } + log("Refactoring ...\n"+option); + Refactor refactor=new Refactor(option); + refactor.run(); + } + private static void logSameLine(String msg){ + Logger.sameLine(getLogTag()+msg); + } + private static void log(String msg){ + Logger.i(getLogTag()+msg); + } + private static String getLogTag(){ + return "[REFACTOR] "; + } + public static boolean isCommand(String command){ + if(Util.isEmpty(command)){ + return false; + } + command=command.toLowerCase().trim(); + return command.equals(ARG_SHORT) || command.equals(ARG_LONG); + } + public static final String ARG_SHORT="x"; + public static final String ARG_LONG="refactor"; + public static final String DESCRIPTION="Refactors obfuscated resource names"; } diff --git a/src/main/java/com/reandroid/apkeditor/refactor/RefactorOptions.java b/src/main/java/com/reandroid/apkeditor/refactor/RefactorOptions.java index c5055ebf..a71757b8 100644 --- a/src/main/java/com/reandroid/apkeditor/refactor/RefactorOptions.java +++ b/src/main/java/com/reandroid/apkeditor/refactor/RefactorOptions.java @@ -1,138 +1,138 @@ - /* - * Copyright (C) 2022 github.com/REAndroid - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.reandroid.apkeditor.refactor; - import com.reandroid.apkeditor.APKEditor; - import com.reandroid.apkeditor.Options; - import com.reandroid.apkeditor.utils.StringHelper; - import com.reandroid.commons.command.ARGException; +import com.reandroid.apkeditor.APKEditor; +import com.reandroid.apkeditor.Options; +import com.reandroid.apkeditor.utils.StringHelper; +import com.reandroid.commons.command.ARGException; - import java.io.File; +import java.io.File; - public class RefactorOptions extends Options { - public File publicXml; - public boolean fixTypeNames; - public boolean cleanMeta; - public RefactorOptions(){ - super(); - } - @Override - public void parse(String[] args) throws ARGException { - parseInput(args); - parseOutput(args); - parsePublicXml(args); - parseFixTypes(args); - parseKeepMeta(args); +public class RefactorOptions extends Options { + public File publicXml; + public boolean fixTypeNames; + public boolean cleanMeta; + public RefactorOptions(){ + super(); + } + @Override + public void parse(String[] args) throws ARGException { + parseInput(args); + parseOutput(args); + parsePublicXml(args); + parseFixTypes(args); + parseKeepMeta(args); - super.parse(args); - } - private void parseKeepMeta(String[] args) throws ARGException { - cleanMeta = containsArg(ARG_cleanMeta, true, args); - } - private void parseFixTypes(String[] args) throws ARGException { - fixTypeNames=containsArg(ARG_fix_types, true, args); - } - private void parsePublicXml(String[] args) throws ARGException { - this.publicXml=null; - File file=parseFile(ARG_public_xml, args); - if(file==null){ - return; - } - if(!file.isFile()){ - throw new ARGException("No such file: "+file); - } - this.publicXml=file; - } - private void parseOutput(String[] args) throws ARGException { - this.outputFile=null; - File file=parseFile(ARG_output, args); - if(file==null){ - file=getOutputApkFromInput(inputFile); - } - this.outputFile=file; - } - private File getOutputApkFromInput(File file){ - String name = file.getName(); - int i=name.lastIndexOf('.'); - if(i>0){ - name=name.substring(0, i); - } - name=name+"_refactored.apk"; - File dir=file.getParentFile(); - if(dir==null){ - return new File(name); - } - return new File(dir, name); - } - private void parseInput(String[] args) throws ARGException { - this.inputFile=null; - File file=parseFile(ARG_input, args); - if(file==null){ - throw new ARGException("Missing input file"); - } - if(!file.isFile()){ - throw new ARGException("No such file: "+file); - } - this.inputFile=file; - } - @Override - public String toString(){ - StringBuilder builder=new StringBuilder(); - builder.append(" Input: ").append(inputFile); - builder.append("\n Output: ").append(outputFile); - if(publicXml!=null){ - builder.append("\n PublicXml: ").append(publicXml); - } - if(force){ - builder.append("\n Force: true"); - } - if(cleanMeta){ - builder.append("\n Keep meta: true"); - } - builder.append("\n ---------------------------- "); - return builder.toString(); - } - public static String getHelp(){ - StringBuilder builder=new StringBuilder(); - builder.append(Refactor.DESCRIPTION); - builder.append("\nOptions:\n"); - String[][] table=new String[][]{ - new String[]{ARG_input, ARG_DESC_input}, - new String[]{ARG_output, ARG_DESC_output}, - new String[]{ARG_public_xml, ARG_DESC_public_xml} - }; - StringHelper.printTwoColumns(builder, " ", 75, table); - builder.append("\nFlags:\n"); - table=new String[][]{ - new String[]{ARG_fix_types, ARG_DESC_fix_types}, - new String[]{ARG_force, ARG_DESC_force}, - new String[]{ARG_cleanMeta, ARG_DESC_cleanMeta} - }; - StringHelper.printTwoColumns(builder, " ", 75, table); - String jar = APKEditor.getJarName(); - builder.append("\n\nExample-1:"); - builder.append("\n java -jar ").append(jar).append(" ").append(Refactor.ARG_SHORT).append(" ") - .append(ARG_input).append(" path/to/input.apk"); - builder.append(" ").append(ARG_output).append(" path/to/out.apk"); - return builder.toString(); - } + super.parse(args); + } + private void parseKeepMeta(String[] args) throws ARGException { + cleanMeta = containsArg(ARG_cleanMeta, true, args); + } + private void parseFixTypes(String[] args) throws ARGException { + fixTypeNames=containsArg(ARG_fix_types, true, args); + } + private void parsePublicXml(String[] args) throws ARGException { + this.publicXml=null; + File file=parseFile(ARG_public_xml, args); + if(file==null){ + return; + } + if(!file.isFile()){ + throw new ARGException("No such file: "+file); + } + this.publicXml=file; + } + private void parseOutput(String[] args) throws ARGException { + this.outputFile=null; + File file=parseFile(ARG_output, args); + if(file==null){ + file=getOutputApkFromInput(inputFile); + } + this.outputFile=file; + } + private File getOutputApkFromInput(File file){ + String name = file.getName(); + int i=name.lastIndexOf('.'); + if(i>0){ + name=name.substring(0, i); + } + name=name+"_refactored.apk"; + File dir=file.getParentFile(); + if(dir==null){ + return new File(name); + } + return new File(dir, name); + } + private void parseInput(String[] args) throws ARGException { + this.inputFile=null; + File file=parseFile(ARG_input, args); + if(file==null){ + throw new ARGException("Missing input file"); + } + if(!file.isFile()){ + throw new ARGException("No such file: "+file); + } + this.inputFile=file; + } + @Override + public String toString(){ + StringBuilder builder=new StringBuilder(); + builder.append(" Input: ").append(inputFile); + builder.append("\n Output: ").append(outputFile); + if(publicXml!=null){ + builder.append("\n PublicXml: ").append(publicXml); + } + if(force){ + builder.append("\n Force: true"); + } + if(cleanMeta){ + builder.append("\n Keep meta: true"); + } + builder.append("\n ---------------------------- "); + return builder.toString(); + } + public static String getHelp(){ + StringBuilder builder=new StringBuilder(); + builder.append(Refactor.DESCRIPTION); + builder.append("\nOptions:\n"); + String[][] table=new String[][]{ + new String[]{ARG_input, ARG_DESC_input}, + new String[]{ARG_output, ARG_DESC_output}, + new String[]{ARG_public_xml, ARG_DESC_public_xml} + }; + StringHelper.printTwoColumns(builder, " ", Options.PRINT_WIDTH, table); + builder.append("\nFlags:\n"); + table=new String[][]{ + new String[]{ARG_fix_types, ARG_DESC_fix_types}, + new String[]{ARG_force, ARG_DESC_force}, + new String[]{ARG_cleanMeta, ARG_DESC_cleanMeta} + }; + StringHelper.printTwoColumns(builder, " ", Options.PRINT_WIDTH, table); + String jar = APKEditor.getJarName(); + builder.append("\n\nExample-1:"); + builder.append("\n java -jar ").append(jar).append(" ").append(Refactor.ARG_SHORT).append(" ") + .append(ARG_input).append(" path/to/input.apk"); + builder.append(" ").append(ARG_output).append(" path/to/out.apk"); + return builder.toString(); + } - private static final String ARG_public_xml = "-public-xml"; - private static final String ARG_DESC_public_xml = "Path of resource ids xml file (public.xml)\nLoads names and applies to resources from 'public.xml' file"; + private static final String ARG_public_xml = "-public-xml"; + private static final String ARG_DESC_public_xml = "Path of resource ids xml file (public.xml)\nLoads names and applies to resources from 'public.xml' file"; - private static final String ARG_fix_types = "-fix-types"; - private static final String ARG_DESC_fix_types = "Corrects resource type names based on usages and values"; + private static final String ARG_fix_types = "-fix-types"; + private static final String ARG_DESC_fix_types = "Corrects resource type names based on usages and values"; } diff --git a/src/main/java/com/reandroid/apkeditor/utils/StringHelper.java b/src/main/java/com/reandroid/apkeditor/utils/StringHelper.java index 08081533..58985202 100644 --- a/src/main/java/com/reandroid/apkeditor/utils/StringHelper.java +++ b/src/main/java/com/reandroid/apkeditor/utils/StringHelper.java @@ -1,165 +1,170 @@ - /* - * Copyright (C) 2022 github.com/REAndroid - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package com.reandroid.apkeditor.utils; +/* + * Copyright (C) 2022 github.com/REAndroid + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.reandroid.apkeditor.utils; - import java.util.ArrayList; - import java.util.Comparator; - import java.util.List; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; - public class StringHelper { - public static String trueOrNull(Boolean value){ - if(value == null){ - return null; - } - return trueOrNull(value.booleanValue()); - } - public static String trueOrNull(boolean value){ - if(!value){ - return null; - } - return String.valueOf(true); - } - public static List sortAscending(List nameList){ - Comparator cmp=new Comparator() { - @Override - public int compare(String s1, String s2) { - return s1.compareTo(s2); - } - }; - nameList.sort(cmp); - return nameList; - } - public static void printNameAndValues(StringBuilder builder, String tab, int totalWidth, Object[][] objTable){ - printNameAndValues(builder, tab, "", totalWidth, objTable); - } - public static void printNameAndValues(StringBuilder builder, String tab, String separator, int totalWidth, Object[][] objTable){ - String[][] table = convertNameAndValue(objTable); - if(table==null){ - return; - } - int leftWidth=0; - for(String[] col:table){ - int len=col[0].length(); - if(len>leftWidth){ - leftWidth=len; - } - } - int bnColumns=0; - leftWidth=leftWidth+bnColumns; - int maxRight=totalWidth-leftWidth; - for(int i=0;i results = new ArrayList<>(); - for(Object[] objRow:table){ - String[] row = convertNameAndValueRow(objRow); - if(row!=null){ - results.add(row); - } - } - if(results.size()==0){ - return null; - } - return results.toArray(new String[0][]); - } - private static String[] convertNameAndValueRow(Object[] objRow){ - if(objRow==null){ - return null; - } - int len = objRow.length; - if(len!=2){ - return null; - } - if(objRow[0] == null || objRow[1] == null){ - return null; - } - String[] result = new String[len]; - result[0] = objRow[0].toString(); - result[1] = objRow[1].toString(); - if(result[0] == null || result[1] == null){ - return null; - } - return result; - } - public static void printTwoColumns(StringBuilder builder, String tab, int totalWidth, String[][] table){ - int leftWidth=0; - for(String[] col:table){ - int len=col[0].length(); - if(len>leftWidth){ - leftWidth=len; - } - } - int bnColumns=3; - leftWidth=leftWidth+bnColumns; - int maxRight=totalWidth-leftWidth; - for(int i=0;i 0 && rightWidth%maxRight==0)){ - builder.append('\n'); - builder.append(tab); - fillSpace(builder, leftWidth+separator.length()); - rightWidth=0; - } - if(ch!='\n'){ - boolean skipFirstSpace=(rightWidth==0 && ch==' '); - if(!skipFirstSpace){ - builder.append(ch); - rightWidth++; - } - } - } - } - private static void fillSpace(StringBuilder builder, int count){ - for(int i=0;i sortAscending(List nameList){ + Comparator cmp=new Comparator() { + @Override + public int compare(String s1, String s2) { + return s1.compareTo(s2); + } + }; + nameList.sort(cmp); + return nameList; + } + public static void printNameAndValues(StringBuilder builder, String tab, int totalWidth, Object[][] objTable){ + printNameAndValues(builder, tab, "", totalWidth, objTable); + } + public static void printNameAndValues(StringBuilder builder, String tab, String separator, int totalWidth, Object[][] objTable){ + String[][] table = convertNameAndValue(objTable); + if(table==null){ + return; + } + int leftWidth=0; + for(String[] col:table){ + int len=col[0].length(); + if(len>leftWidth){ + leftWidth=len; + } + } + int bnColumns=0; + leftWidth=leftWidth+bnColumns; + int maxRight=totalWidth-leftWidth; + for(int i=0;i results = new ArrayList<>(); + for(Object[] objRow:table){ + String[] row = convertNameAndValueRow(objRow); + if(row!=null){ + results.add(row); + } + } + if(results.size()==0){ + return null; + } + return results.toArray(new String[0][]); + } + private static String[] convertNameAndValueRow(Object[] objRow){ + if(objRow==null){ + return null; + } + int len = objRow.length; + if(len!=2){ + return null; + } + if(objRow[0] == null || objRow[1] == null){ + return null; + } + String[] result = new String[len]; + result[0] = objRow[0].toString(); + result[1] = objRow[1].toString(); + if(result[0] == null || result[1] == null){ + return null; + } + return result; + } + public static void printTwoColumns(StringBuilder builder, String tab, int totalWidth, String[][] table){ + printTwoColumns(builder, tab, " ", totalWidth, table); + } + public static void printTwoColumns(StringBuilder builder, String tab, String columnSeparator, int totalWidth, String[][] table){ + int leftWidth = 0; + for(String[] col:table){ + int len = col[0].length(); + if(len > leftWidth){ + leftWidth = len; + } + } + int maxRight = totalWidth - leftWidth; + for(int i=0;i 0 && rightWidth%maxRight==0)){ + builder.append('\n'); + builder.append(tab); + fillSpace(builder, leftWidth+separator.length()); + rightWidth = 0; + spacePrefixSeen = false; + } + if(ch!='\n'){ + boolean skipFirstSpace=(rightWidth==0 && ch==' '); + if(!skipFirstSpace || spacePrefixSeen){ + builder.append(ch); + rightWidth++; + }else{ + spacePrefixSeen = true; + } + } + } + } + private static void fillSpace(StringBuilder builder, int count){ + for(int i=0;i