diff --git a/libs/ARSCLib.jar b/libs/ARSCLib.jar index b00679a3..a7ebc823 100644 Binary files a/libs/ARSCLib.jar and b/libs/ARSCLib.jar differ diff --git a/src/main/java/com/reandroid/apkeditor/Options.java b/src/main/java/com/reandroid/apkeditor/Options.java index a48d2cd4..079334bd 100644 --- a/src/main/java/com/reandroid/apkeditor/Options.java +++ b/src/main/java/com/reandroid/apkeditor/Options.java @@ -46,8 +46,11 @@ protected void parseFrameworkVersion(String[] args) throws ARGException { } } protected void parseType(String[] args) throws ARGException { + parseType(args, TYPE_JSON); + } + protected void parseType(String[] args, String def) throws ARGException { String[] choices = new String[]{TYPE_JSON, TYPE_XML, TYPE_SIG}; - this.type = parseType(ARG_type, args, choices, TYPE_JSON); + this.type = parseType(ARG_type, args, choices, def); } protected void parseSignaturesDir(String[] args) throws ARGException { this.signaturesDirectory = parseFile(ARG_sig, args); diff --git a/src/main/java/com/reandroid/apkeditor/compile/Builder.java b/src/main/java/com/reandroid/apkeditor/compile/Builder.java index a17d7b9f..0bdae91f 100644 --- a/src/main/java/com/reandroid/apkeditor/compile/Builder.java +++ b/src/main/java/com/reandroid/apkeditor/compile/Builder.java @@ -15,6 +15,7 @@ */ package com.reandroid.apkeditor.compile; +import com.reandroid.apk.*; import com.reandroid.apkeditor.Util; import com.reandroid.archive.WriteProgress; import com.reandroid.archive2.Archive; @@ -22,12 +23,7 @@ import com.reandroid.archive2.writer.ApkWriter; import com.reandroid.commons.command.ARGException; import com.reandroid.commons.utils.log.Logger; -import com.reandroid.apk.APKLogger; -import com.reandroid.apk.ApkJsonEncoder; -import com.reandroid.apk.ApkModule; -import com.reandroid.apk.ApkModuleXmlEncoder; import com.reandroid.arsc.chunk.xml.AndroidManifestBlock; -import com.reandroid.xml.XMLException; import java.io.File; import java.io.IOException; @@ -64,9 +60,10 @@ private void restoreSignatures() throws IOException { } public void buildJson() throws IOException { log("Scanning JSON directory ..."); - ApkJsonEncoder encoder=new ApkJsonEncoder(); - encoder.setAPKLogger(getAPKLogger()); - ApkModule loadedModule=encoder.scanDirectory(options.inputFile); + ApkModuleJsonEncoder encoder=new ApkModuleJsonEncoder(); + encoder.setApkLogger(getAPKLogger()); + encoder.scanDirectory(options.inputFile); + ApkModule loadedModule = encoder.getApkModule(); loadedModule.setAPKLogger(getAPKLogger()); if(options.resDirName!=null){ log("Renaming resources root dir: "+options.resDirName); @@ -86,15 +83,10 @@ public void buildXml() throws IOException { log("Scanning XML directory ..."); ApkModuleXmlEncoder encoder=new ApkModuleXmlEncoder(); encoder.setApkLogger(getAPKLogger()); - ApkModule loadedModule; - try { - loadedModule=encoder.getApkModule(); - loadedModule.setPreferredFramework(options.frameworkVersion); - encoder.scanDirectory(options.inputFile); - loadedModule=encoder.getApkModule(); - } catch (XMLException ex) { - throw new IOException(ex.getMessage(), ex); - } + ApkModule loadedModule = encoder.getApkModule(); + loadedModule.setPreferredFramework(options.frameworkVersion); + encoder.scanDirectory(options.inputFile); + loadedModule = encoder.getApkModule(); log("Writing apk..."); loadedModule.writeApk(options.outputFile, null); log("Built to: "+options.outputFile); @@ -161,6 +153,9 @@ public static void execute(String[] args) throws ARGException, IOException { } private static boolean isXmlInDir(File dir){ File manifest=new File(dir, AndroidManifestBlock.FILE_NAME); + if(!manifest.isFile()){ + manifest=new File(dir, AndroidManifestBlock.FILE_NAME_BIN); + } return manifest.isFile(); } private static boolean isJsonInDir(File dir) { diff --git a/src/main/java/com/reandroid/apkeditor/decompile/DecompileOptions.java b/src/main/java/com/reandroid/apkeditor/decompile/DecompileOptions.java index 16358237..9267ad25 100644 --- a/src/main/java/com/reandroid/apkeditor/decompile/DecompileOptions.java +++ b/src/main/java/com/reandroid/apkeditor/decompile/DecompileOptions.java @@ -27,23 +27,28 @@ public class DecompileOptions extends Options { public boolean splitJson; public boolean validateResDir; public String resDirName; + public boolean keepResPath; public DecompileOptions(){ - type=TYPE_JSON; + type=TYPE_XML; } @Override public void parse(String[] args) throws ARGException { parseInput(args); - parseType(args); + parseType(args, type); parseOutput(args); parseSplitResources(args); + parseKeepResPath(args); parseResDirName(args); parseValidateResDir(args); parseSignaturesDir(args); if(signaturesDirectory == null && type == null){ - type = TYPE_JSON; + type = TYPE_XML; } super.parse(args); } + private void parseKeepResPath(String[] args) throws ARGException { + keepResPath = containsArg(ARG_keep_res_path, true, args); + } private void parseValidateResDir(String[] args) throws ARGException { validateResDir=containsArg(ARG_validate_res_dir, true, args); } @@ -106,6 +111,9 @@ public String toString(){ if(force){ builder.append("\n Force: true"); } + if(keepResPath){ + builder.append("\n Keep res path: true"); + } if(frameworkVersion != null){ builder.append("\nframework: ").append(frameworkVersion); } @@ -132,6 +140,7 @@ public static String getHelp(){ builder.append("\nFlags:\n"); table=new String[][]{ new String[]{ARG_force, ARG_DESC_force}, + new String[]{ARG_keep_res_path, ARG_DESC_keep_res_path}, new String[]{ARG_split_resources, ARG_DESC_split_resources}, new String[]{ARG_validate_res_dir, ARG_DESC_validate_res_dir} }; @@ -161,8 +170,11 @@ public static String getHelp(){ private static final String ARG_split_resources="-split-json"; private static final String ARG_DESC_split_resources="splits resources.arsc into multiple parts as per type entries (use this for large files)"; - private 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"; + private static final String ARG_DESC_type = "Decode types: \n 1) json \n 2) xml \n 3) sig \n default=" + TYPE_XML; + + + private static final String ARG_keep_res_path = "-keep-res-path"; + private static final String ARG_DESC_keep_res_path = "Keeps original res/* file paths:" + "\n *Applies only when decoding to xml" + + "\n *All res/* files will be placed on dir \n *The relative paths will be linked to values/*xml"; } diff --git a/src/main/java/com/reandroid/apkeditor/decompile/Decompiler.java b/src/main/java/com/reandroid/apkeditor/decompile/Decompiler.java index 10445a8c..bd638de5 100644 --- a/src/main/java/com/reandroid/apkeditor/decompile/Decompiler.java +++ b/src/main/java/com/reandroid/apkeditor/decompile/Decompiler.java @@ -15,15 +15,12 @@ */ package com.reandroid.apkeditor.decompile; +import com.reandroid.apk.*; import com.reandroid.apkeditor.Util; import com.reandroid.archive2.Archive; import com.reandroid.archive2.block.ApkSignatureBlock; import com.reandroid.commons.command.ARGException; import com.reandroid.commons.utils.log.Logger; -import com.reandroid.apk.APKLogger; -import com.reandroid.apk.ApkJsonDecoder; -import com.reandroid.apk.ApkModule; -import com.reandroid.apk.ApkModuleXmlDecoder; import java.io.File; import java.io.IOException; @@ -59,15 +56,16 @@ public void run() throws IOException { } if(DecompileOptions.TYPE_JSON.equals(options.type)){ log("Decompiling to JSON ..."); - ApkJsonDecoder decoder=new ApkJsonDecoder(apkModule, options.splitJson); + ApkModuleJsonDecoder decoder = new ApkModuleJsonDecoder(apkModule, options.splitJson); decoder.sanitizeFilePaths(); - decoder.writeToDirectory(options.outputFile); + decoder.decode(options.outputFile); }else{ log("Decompiling to XML ..."); - ApkModuleXmlDecoder xmlDecoder=new ApkModuleXmlDecoder(apkModule); + ApkModuleXmlDecoder xmlDecoder = new ApkModuleXmlDecoder(apkModule); + xmlDecoder.setKeepResPath(options.keepResPath); xmlDecoder.sanitizeFilePaths(); try { - xmlDecoder.decodeTo(options.outputFile); + xmlDecoder.decode(options.outputFile); } catch (Exception ex) { throw new IOException(ex.getMessage(), ex); } diff --git a/src/main/java/com/reandroid/apkeditor/refactor/AutoRefactor.java b/src/main/java/com/reandroid/apkeditor/refactor/AutoRefactor.java index 3f602ff6..d4c8dce4 100644 --- a/src/main/java/com/reandroid/apkeditor/refactor/AutoRefactor.java +++ b/src/main/java/com/reandroid/apkeditor/refactor/AutoRefactor.java @@ -1,4 +1,4 @@ - /* +/* * Copyright (C) 2022 github.com/REAndroid * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,31 +15,34 @@ */ package com.reandroid.apkeditor.refactor; +import com.reandroid.apk.APKLogger; import com.reandroid.apk.ApkModule; import com.reandroid.apk.ResFile; -import com.reandroid.apk.ResourceIds; import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.identifiers.PackageIdentifier; +import com.reandroid.identifiers.TableIdentifier; +import com.reandroid.identifiers.TypeIdentifier; import java.io.IOException; import java.util.List; public class AutoRefactor { private final ApkModule mApkModule; + private APKLogger apkLogger; public AutoRefactor(ApkModule apkModule){ - this.mApkModule=apkModule; + this.mApkModule = apkModule; + this.apkLogger = apkModule.getApkLogger(); } - public int refactor() throws IOException { - ResourceIds refactoredId=buildRefactor(); - TableBlock tableBlock=mApkModule.getTableBlock(); - int renameCount=refactoredId.applyTo(tableBlock); + public void refactor() throws IOException { + refactorResourceNames(); refactorFilePaths(); - return renameCount; } - public int refactorFilePaths() throws IOException { - int renameCount=0; + public int refactorFilePaths(){ + logMessage("Validating file paths ..."); + int renameCount = 0; List resFileList = mApkModule.listResFiles(); for(ResFile resFile:resFileList){ - String path=RefactorUtil.RES_DIR+"/"+resFile.buildPath(); + String path = RefactorUtil.RES_DIR + "/" + resFile.buildPath(); if(path.equals(resFile.getFilePath())){ continue; } @@ -48,24 +51,45 @@ public int refactorFilePaths() throws IOException { } return renameCount; } - private ResourceIds buildRefactor() throws IOException { - ResourceIds.Table obfTable=new ResourceIds.Table(); - ResourceIds resourceIds=new ResourceIds(obfTable); - TableBlock tableBlock=mApkModule.getTableBlock(); - resourceIds.loadTableBlock(tableBlock); - ResourceIds.Table table=new ResourceIds.Table(); - for(ResourceIds.Table.Package obfPackage: obfTable.listPackages()){ - ResourceIds.Table.Package pkg=new ResourceIds.Table.Package(obfPackage.id); - pkg.name=obfPackage.name; - for(ResourceIds.Table.Package.Type obfType:obfPackage.listTypes()){ - EntryRefactor entryRefactor=new EntryRefactor(tableBlock, obfType); - if(!entryRefactor.isObfuscated()){ - continue; - } - pkg.add(entryRefactor.refactorAll()); + private void refactorResourceNames(){ + logMessage("Validating resource names ..."); + TableIdentifier tableIdentifier = new TableIdentifier(); + TableBlock tableBlock = mApkModule.getTableBlock(); + tableIdentifier.load(tableBlock); + String msg = tableIdentifier.validateSpecNames(); + if(msg == null){ + logMessage("All resource names are valid"); + return; + } + int count = 0; + for(PackageIdentifier pi: tableIdentifier.getPackages()){ + for(TypeIdentifier ti:pi.list()){ + EntryRefactor entryRefactor = new EntryRefactor(ti); + count += entryRefactor.refactorAll(); } - table.add(pkg); } - return new ResourceIds(table); + logMessage(msg); + } + public void setApkLogger(APKLogger apkLogger) { + this.apkLogger = apkLogger; + } + void logMessage(String msg) { + APKLogger apkLogger = this.apkLogger; + if(apkLogger!=null){ + apkLogger.logMessage(msg); + } + } + void logError(String msg, Throwable tr) { + APKLogger apkLogger = this.apkLogger; + if(apkLogger == null || (msg == null && tr == null)){ + return; + } + apkLogger.logError(msg, tr); + } + void logVerbose(String msg) { + APKLogger apkLogger = this.apkLogger; + if(apkLogger!=null){ + apkLogger.logVerbose(msg); + } } } diff --git a/src/main/java/com/reandroid/apkeditor/refactor/EntryRefactor.java b/src/main/java/com/reandroid/apkeditor/refactor/EntryRefactor.java index adf35651..e2dca668 100644 --- a/src/main/java/com/reandroid/apkeditor/refactor/EntryRefactor.java +++ b/src/main/java/com/reandroid/apkeditor/refactor/EntryRefactor.java @@ -1,102 +1,52 @@ - /* - * 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.apk.ResourceIds; -import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.identifiers.ResourceIdentifier; +import com.reandroid.identifiers.TypeIdentifier; -import java.util.*; - - public class EntryRefactor { - private final TableBlock mTableBlock; - private final ResourceIds.Table.Package.Type mType; - public EntryRefactor(TableBlock tableBlock, ResourceIds.Table.Package.Type type){ - this.mTableBlock=tableBlock; - this.mType=type; - } - public ResourceIds.Table.Package.Type refactorAll(){ - ResourceIds.Table.Package.Type result=new ResourceIds.Table.Package.Type(mType.getId()); - for(ResourceIds.Table.Package.Type.Entry entry:mType.listEntries()){ - result.add(refactor(entry)); - } - return result; - } - private ResourceIds.Table.Package.Type.Entry refactor(ResourceIds.Table.Package.Type.Entry entry){ - ResourceIds.Table.Package.Type.Entry result; - result=refactorByValue(entry); - if(result==null){ - result=refactorGenerateName(entry); - } - return result; - } - /* +public class EntryRefactor { + private final TypeIdentifier mTypeIdentifier; + public EntryRefactor(TypeIdentifier typeIdentifier){ + this.mTypeIdentifier = typeIdentifier; + } + public int refactorAll(){ + int result = 0; + for(ResourceIdentifier ri : mTypeIdentifier.getItems()){ + if(!ri.isGeneratedName()){ + continue; + } + boolean renamed = refactor(ri); + if(renamed){ + result ++; + } + } + return result; + } + private boolean refactor(ResourceIdentifier entry){ + return refactorByValue(entry); + } + /* * TODO: implement refactoring from TableBlock entry value * e.g-1: No internet connection * ==> No internet connection * e.g-2: #FF0000 * ==> #FF0000 */ - private ResourceIds.Table.Package.Type.Entry refactorByValue(ResourceIds.Table.Package.Type.Entry entry){ - return null; - } - private ResourceIds.Table.Package.Type.Entry refactorGenerateName(ResourceIds.Table.Package.Type.Entry entry){ - int resourceId=entry.getResourceId(); - - String name=RefactorUtil.generateUniqueName( - entry.getTypeName(), - entry.getResourceId()); - - return new ResourceIds.Table.Package.Type.Entry( - resourceId, - entry.getTypeName(), name); - } - public boolean isObfuscated(){ - Set entryNames=getEntryNames(); - if(entryNames.size()!=mType.entryMap.size()){ - //duplicates found - return true; - } - return RefactorUtil.isObfuscated(entryNames); - } - private Set getEntryNames(){ - Set results=new HashSet<>(); - for(ResourceIds.Table.Package.Type.Entry entry:mType.listEntries()){ - results.add(entry.name); - } - return results; - } - private boolean contains(String entryName){ - for(ResourceIds.Table.Package.Type.Entry entry:mType.listEntries()){ - if(entryName.equals(entry.name)){ - return true; - } - } - return false; - } - private boolean isUnique(String entryName){ - boolean firstFound=false; - for(ResourceIds.Table.Package.Type.Entry entry:mType.listEntries()){ - if(entryName.equals(entry.name)){ - if(!firstFound){ - firstFound=true; - continue; - } - return false; - } - } - return true; - } + private boolean refactorByValue(ResourceIdentifier resourceIdentifier){ + return false; + } } diff --git a/src/main/java/com/reandroid/apkeditor/refactor/PublicXmlRefactor.java b/src/main/java/com/reandroid/apkeditor/refactor/PublicXmlRefactor.java index f6c84406..bb789e72 100644 --- a/src/main/java/com/reandroid/apkeditor/refactor/PublicXmlRefactor.java +++ b/src/main/java/com/reandroid/apkeditor/refactor/PublicXmlRefactor.java @@ -1,22 +1,24 @@ - /* - * 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.apk.APKLogger; import com.reandroid.apk.ApkModule; -import com.reandroid.apk.ResourceIds; +import com.reandroid.identifiers.TableIdentifier; +import org.xmlpull.v1.XmlPullParserException; import java.io.File; import java.io.IOException; @@ -24,13 +26,49 @@ public class PublicXmlRefactor { private final ApkModule apkModule; private final File pubXmlFile; + private APKLogger apkLogger; public PublicXmlRefactor(ApkModule apkModule, File pubXmlFile){ - this.apkModule=apkModule; - this.pubXmlFile=pubXmlFile; + this.apkModule = apkModule; + this.pubXmlFile = pubXmlFile; + this.apkLogger = apkModule.getApkLogger(); } - public int refactor() throws IOException { - ResourceIds resourceIds=new ResourceIds(); - resourceIds.fromXml(pubXmlFile); - return resourceIds.applyTo(apkModule.getTableBlock()); + public void refactor() throws IOException { + logMessage("Loading: " + pubXmlFile); + TableIdentifier tableIdentifier = new TableIdentifier(); + try { + tableIdentifier.loadPublicXml(pubXmlFile); + } catch (XmlPullParserException ex) { + throw new IOException(ex); + } + logMessage("Applying from public xml ..."); + tableIdentifier.setTableBlock(apkModule.getTableBlock()); + int count = tableIdentifier.renameSpecs(); + if(count == 0){ + logMessage("Nothing renamed !"); + } + logMessage("Renamed: " + count); + apkModule.getTableBlock().removeUnusedSpecs(); + } + public void setApkLogger(APKLogger apkLogger) { + this.apkLogger = apkLogger; + } + void logMessage(String msg) { + APKLogger apkLogger = this.apkLogger; + if(apkLogger!=null){ + apkLogger.logMessage(msg); + } + } + void logError(String msg, Throwable tr) { + APKLogger apkLogger = this.apkLogger; + if(apkLogger == null || (msg == null && tr == null)){ + return; + } + apkLogger.logError(msg, tr); + } + void logVerbose(String msg) { + APKLogger apkLogger = this.apkLogger; + if(apkLogger!=null){ + apkLogger.logVerbose(msg); + } } } diff --git a/src/main/java/com/reandroid/apkeditor/refactor/Refactor.java b/src/main/java/com/reandroid/apkeditor/refactor/Refactor.java index 537aa165..3254949d 100644 --- a/src/main/java/com/reandroid/apkeditor/refactor/Refactor.java +++ b/src/main/java/com/reandroid/apkeditor/refactor/Refactor.java @@ -50,18 +50,18 @@ public void run() throws IOException { 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); + 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); + publicXmlRefactor.refactor(); + }else { + log("Auto refactoring ..."); + AutoRefactor autoRefactor=new AutoRefactor(module); + autoRefactor.refactor(); + log("Auto renamed entries"); + StringValueNameGenerator generator = new StringValueNameGenerator(module.getTableBlock()); + generator.refactor(); } if(options.cleanMeta){ log("Clearing META-INF ..."); diff --git a/src/main/java/com/reandroid/apkeditor/refactor/RefactorUtil.java b/src/main/java/com/reandroid/apkeditor/refactor/RefactorUtil.java index 010687dd..a7f9b32c 100644 --- a/src/main/java/com/reandroid/apkeditor/refactor/RefactorUtil.java +++ b/src/main/java/com/reandroid/apkeditor/refactor/RefactorUtil.java @@ -31,6 +31,9 @@ public static boolean isObfuscated(Collection entryNames){ if(isAllEqualLength(entryNames)){ return true; } + if(isAllShortLength(entryNames)){ + return true; + } if(isSequentialNames(entryNames)){ return true; } @@ -60,6 +63,20 @@ private static boolean isAllEqualLength(Collection entryNames){ } return length!=0; } + private static boolean isAllShortLength(Collection entryNames){ + int length=0; + for(String name:entryNames){ + int len=name.length(); + if(length==0){ + length=len; + continue; + } + if(len>3){ + return false; + } + } + return entryNames.size()>10; + } public static boolean isSequentialNames(Collection entryNames){ if(entryNames.size()==0){ return false;