diff --git a/src/main/java/com/reandroid/apk/ApkJsonDecoder.java b/src/main/java/com/reandroid/apk/ApkJsonDecoder.java index 71b0d36..935cdec 100644 --- a/src/main/java/com/reandroid/apk/ApkJsonDecoder.java +++ b/src/main/java/com/reandroid/apk/ApkJsonDecoder.java @@ -39,6 +39,10 @@ public class ApkJsonDecoder { public ApkJsonDecoder(ApkModule apkModule){ this(apkModule, false); } + public void sanitizeFilePaths(){ + PathSanitizer sanitizer = new PathSanitizer(apkModule); + sanitizer.sanitize(); + } public File writeToDirectory(File dir) throws IOException { this.decodedPaths.clear(); writeUncompressed(dir); @@ -48,8 +52,15 @@ public class ApkJsonDecoder { //writePublicXml(dir); writeResources(dir); writeRootFiles(dir); + writePathMap(dir); return new File(dir, apkModule.getModuleName()); } + private void writePathMap(File dir) throws IOException { + PathMap pathMap = new PathMap(); + pathMap.add(apkModule.getApkArchive()); + File file = toPathMapJsonFile(dir); + pathMap.toJson().write(file); + } private void writeUncompressed(File dir) throws IOException { File file=toUncompressedJsonFile(dir); UncompressedFiles uncompressedFiles=new UncompressedFiles(); @@ -77,17 +88,9 @@ public class ApkJsonDecoder { jsonObject.write(file); addDecoded(path); } - // TODO: temporary fix private void writeRootFiles(File dir) throws IOException { for(InputSource inputSource:apkModule.getApkArchive().listInputSources()){ - try{ - writeRootFile(dir, inputSource); - }catch (IOException ex){ - APKLogger logger = apkModule.getApkLogger(); - if(logger!=null){ - logger.logMessage("ERROR: "+ex.getMessage()); - } - } + writeRootFile(dir, inputSource); } } private void writeRootFile(File dir, InputSource inputSource) throws IOException { @@ -186,6 +189,10 @@ public class ApkJsonDecoder { String name = "public.xml"; return new File(file, name); } + private File toPathMapJsonFile(File dir){ + File file = new File(dir, apkModule.getModuleName()); + return new File(file, PathMap.JSON_FILE); + } private File toUncompressedJsonFile(File dir){ File file = new File(dir, apkModule.getModuleName()); return new File(file, UncompressedFiles.JSON_FILE); diff --git a/src/main/java/com/reandroid/apk/ApkJsonEncoder.java b/src/main/java/com/reandroid/apk/ApkJsonEncoder.java index 942f521..96a10ff 100644 --- a/src/main/java/com/reandroid/apk/ApkJsonEncoder.java +++ b/src/main/java/com/reandroid/apk/ApkJsonEncoder.java @@ -19,8 +19,11 @@ import com.reandroid.archive.APKArchive; import com.reandroid.archive.FileInputSource; import com.reandroid.arsc.chunk.TableBlock; import com.reandroid.arsc.chunk.xml.AndroidManifestBlock; +import com.reandroid.json.JSONArray; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.util.List; @@ -40,8 +43,27 @@ public class ApkJsonEncoder { module.setAPKLogger(apkLogger); loadUncompressed(module, moduleDir); applyResourceId(module, moduleDir); + restorePathMap(moduleDir, module); return module; } + private void restorePathMap(File dir, ApkModule apkModule){ + File file = new File(dir, PathMap.JSON_FILE); + if(!file.isFile()){ + return; + } + logMessage("Restoring file path ..."); + PathMap pathMap = new PathMap(); + FileInputStream inputStream = null; + try { + inputStream = new FileInputStream(file); + } catch (FileNotFoundException exception) { + logError("Failed to load path-map", exception); + return; + } + JSONArray jsonArray = new JSONArray(inputStream); + pathMap.fromJson(jsonArray); + pathMap.restore(apkModule); + } private void applyResourceId(ApkModule apkModule, File moduleDir) { if(!apkModule.hasTableBlock()){ return; diff --git a/src/main/java/com/reandroid/apk/ApkModuleXmlDecoder.java b/src/main/java/com/reandroid/apk/ApkModuleXmlDecoder.java index 9c64836..b2acb62 100644 --- a/src/main/java/com/reandroid/apk/ApkModuleXmlDecoder.java +++ b/src/main/java/com/reandroid/apk/ApkModuleXmlDecoder.java @@ -48,6 +48,10 @@ import java.util.*; this.decodedEntries = new HashMap<>(); this.mDecodedPaths = new HashSet<>(); } + public void sanitizeFilePaths(){ + PathSanitizer sanitizer = new PathSanitizer(apkModule); + sanitizer.sanitize(); + } public void decodeTo(File outDir) throws IOException, XMLException { this.decodedEntries.clear(); @@ -74,6 +78,14 @@ import java.util.*; decodeValues(tableBlock, outDir, tableBlock); extractRootFiles(outDir); + + writePathMap(outDir); + } + private void writePathMap(File dir) throws IOException { + PathMap pathMap = new PathMap(); + pathMap.add(apkModule.getApkArchive()); + File file = new File(dir, PathMap.JSON_FILE); + pathMap.toJson().write(file); } private void decodePackageInfo(File outDir, TableBlock tableBlock) throws IOException { for(PackageBlock packageBlock:tableBlock.listPackages()){ @@ -103,22 +115,21 @@ import java.util.*; } private void decodeResRaw(File outDir, ResFile resFile) throws IOException { - Entry entry =resFile.pickOne(); + Entry entry = resFile.pickOne(); PackageBlock packageBlock= entry.getPackageBlock(); File pkgDir=new File(outDir, getPackageDirName(packageBlock)); - File resDir=new File(pkgDir, ApkUtil.RES_DIR_NAME); - String path=resFile.buildPath(); - path=path.replace('/', File.separatorChar); - File file=new File(resDir, path); + String alias = resFile.buildPath(ApkUtil.RES_DIR_NAME); + String path = alias.replace('/', File.separatorChar); + File file=new File(pkgDir, path); File dir=file.getParentFile(); if(!dir.exists()){ dir.mkdirs(); } - FileOutputStream outputStream=new FileOutputStream(file); resFile.getInputSource().write(outputStream); outputStream.close(); + resFile.setFilePath(alias); addDecodedEntry(entry); } @@ -130,10 +141,10 @@ import java.util.*; resFile.getInputSource().getName()); File pkgDir=new File(outDir, getPackageDirName(packageBlock)); - File resDir=new File(pkgDir, ApkUtil.RES_DIR_NAME); - String path=resFile.buildPath(); + String alias = resFile.buildPath(ApkUtil.RES_DIR_NAME); + String path = alias.replace('/', File.separatorChar); path=path.replace('/', File.separatorChar); - File file=new File(resDir, path); + File file=new File(pkgDir, path); logVerbose("Decoding: "+path); XMLNamespaceValidator namespaceValidator=new XMLNamespaceValidator(resXmlDocument); @@ -141,6 +152,8 @@ import java.util.*; XMLDocument xmlDocument= resXmlDocument.decodeToXml(entryStore, packageBlock.getId()); xmlDocument.save(file, true); + resFile.setFilePath(alias); + addDecodedEntry(resFile.pickOne()); } private void decodePublicXml(TableBlock tableBlock, File outDir) @@ -304,14 +317,9 @@ import java.util.*; if(!dir.exists()){ dir.mkdirs(); } - // TODO:Temporary fix - try{ - FileOutputStream outputStream=new FileOutputStream(file); - inputSource.write(outputStream); - outputStream.close(); - }catch (IOException ex){ - logMessage("ERROR: "+ex.getMessage()); - } + FileOutputStream outputStream=new FileOutputStream(file); + inputSource.write(outputStream); + outputStream.close(); } private boolean containsDecodedPath(String path){ return mDecodedPaths.contains(path); diff --git a/src/main/java/com/reandroid/apk/ApkModuleXmlEncoder.java b/src/main/java/com/reandroid/apk/ApkModuleXmlEncoder.java index 84b180d..0b0c293 100644 --- a/src/main/java/com/reandroid/apk/ApkModuleXmlEncoder.java +++ b/src/main/java/com/reandroid/apk/ApkModuleXmlEncoder.java @@ -19,9 +19,11 @@ package com.reandroid.apk; import com.reandroid.archive.FileInputSource; import com.reandroid.apk.xmlencoder.RESEncoder; import com.reandroid.arsc.chunk.TableBlock; + import com.reandroid.json.JSONArray; import com.reandroid.xml.XMLException; import java.io.File; + import java.io.FileInputStream; import java.io.IOException; import java.util.List; @@ -38,6 +40,18 @@ package com.reandroid.apk; resEncoder.scanDirectory(mainDirectory); File rootDir=new File(mainDirectory, "root"); scanRootDir(rootDir); + restorePathMap(mainDirectory); + } + private void restorePathMap(File dir) throws IOException{ + File file = new File(dir, PathMap.JSON_FILE); + if(!file.isFile()){ + return; + } + PathMap pathMap = new PathMap(); + FileInputStream inputStream = new FileInputStream(file); + JSONArray jsonArray = new JSONArray(inputStream); + pathMap.fromJson(jsonArray); + pathMap.restore(getApkModule()); } public ApkModule getApkModule(){ return resEncoder.getApkModule(); diff --git a/src/main/java/com/reandroid/apk/PathMap.java b/src/main/java/com/reandroid/apk/PathMap.java new file mode 100644 index 0000000..aa2a059 --- /dev/null +++ b/src/main/java/com/reandroid/apk/PathMap.java @@ -0,0 +1,206 @@ + /* + * 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.apk; + +import com.reandroid.archive.InputSource; +import com.reandroid.archive.ZipArchive; +import com.reandroid.json.JSONArray; +import com.reandroid.json.JSONConvert; +import com.reandroid.json.JSONObject; + +import java.util.*; + +public class PathMap implements JSONConvert { + private final Object mLock = new Object(); + private final Map mNameAliasMap; + private final Map mAliasNameMap; + + public PathMap(){ + this.mNameAliasMap = new HashMap<>(); + this.mAliasNameMap = new HashMap<>(); + } + + public void restore(ApkModule apkModule){ + restoreResFile(apkModule.listResFiles()); + restore(apkModule.getApkArchive().listInputSources()); + } + public List restoreResFile(Collection files){ + List results = new ArrayList<>(); + if(files == null){ + return results; + } + for(ResFile resFile:files){ + String alias = restoreResFile(resFile); + if(alias==null){ + continue; + } + results.add(alias); + } + return results; + } + public String restoreResFile(ResFile resFile){ + InputSource inputSource = resFile.getInputSource(); + String alias = restore(inputSource); + if(alias==null){ + return null; + } + resFile.setFilePath(alias); + return alias; + } + public List restore(Collection sources){ + List results = new ArrayList<>(); + if(sources == null){ + return results; + } + for(InputSource inputSource:sources){ + String alias = restore(inputSource); + if(alias==null){ + continue; + } + results.add(alias); + } + return results; + } + public String restore(InputSource inputSource){ + if(inputSource==null){ + return null; + } + String name = inputSource.getName(); + String alias = getName(name); + if(alias==null){ + name = inputSource.getAlias(); + alias = getName(name); + } + if(alias==null || alias.equals(inputSource.getAlias())){ + return null; + } + inputSource.setAlias(alias); + return alias; + } + + public String getAlias(String name){ + synchronized (mLock){ + return mNameAliasMap.get(name); + } + } + public String getName(String alias){ + synchronized (mLock){ + return mAliasNameMap.get(alias); + } + } + public int size(){ + synchronized (mLock){ + return mNameAliasMap.size(); + } + } + public void clear(){ + synchronized (mLock){ + mNameAliasMap.clear(); + mAliasNameMap.clear(); + } + } + public void add(ZipArchive archive){ + if(archive == null){ + return; + } + add(archive.listInputSources()); + } + public void add(Collection sources){ + if(sources==null){ + return; + } + for(InputSource inputSource:sources){ + add(inputSource); + } + } + public void add(InputSource inputSource){ + if(inputSource==null){ + return; + } + add(inputSource.getName(), inputSource.getAlias()); + } + public void add(String name, String alias){ + if(name==null || alias==null){ + return; + } + if(name.equals(alias)){ + return; + } + synchronized (mLock){ + mNameAliasMap.remove(name); + mNameAliasMap.put(name, alias); + mAliasNameMap.remove(alias); + mAliasNameMap.put(alias, name); + } + } + + private void add(JSONObject json){ + if(json==null){ + return; + } + add(json.optString(NAME_name), json.optString(NAME_alias)); + } + + @Override + public JSONArray toJson() { + JSONArray jsonArray = new JSONArray(); + Map nameMap = this.mNameAliasMap; + List nameList = toSortedList(nameMap.keySet()); + for(String name:nameList){ + JSONObject jsonObject = new JSONObject(); + jsonObject.put(NAME_name, name); + jsonObject.put(NAME_alias, nameMap.get(name)); + jsonArray.put(jsonObject); + } + return jsonArray; + } + @Override + public void fromJson(JSONArray json) { + clear(); + if(json==null){ + return; + } + int length = json.length(); + for(int i=0;i mSanitizedPaths; + public PathSanitizer(ApkModule apkModule){ + this.apkModule = apkModule; + this.apkLogger = apkModule.getApkLogger(); + this.mSanitizedPaths = new HashSet<>(); + } + public void sanitize(){ + mSanitizedPaths.clear(); + logMessage("Sanitizing paths ..."); + List resFileList = apkModule.listResFiles(); + for(ResFile resFile:resFileList){ + sanitize(resFile); + } + List sourceList = apkModule.getApkArchive().listInputSources(); + for(InputSource inputSource:sourceList){ + sanitize(inputSource, 1, false); + } + logMessage("DONE = "+mSanitizedPaths.size()); + } + private void sanitize(ResFile resFile){ + InputSource inputSource = resFile.getInputSource(); + String replace = sanitize(inputSource, 3, true); + if(replace==null){ + return; + } + resFile.setFilePath(replace); + } + private String sanitize(InputSource inputSource, int depth, boolean fixedDepth){ + String name = inputSource.getName(); + if(mSanitizedPaths.contains(name)){ + return null; + } + mSanitizedPaths.add(name); + String alias = inputSource.getAlias(); + String replace = sanitize(alias, depth, fixedDepth); + if(alias.equals(replace)){ + return null; + } + inputSource.setAlias(replace); + logVerbose("REN: '"+alias+"' -> '"+replace+"'"); + return replace; + } + + private String sanitize(String name, int depth, boolean fixedDepth){ + StringBuilder builder = new StringBuilder(); + String[] nameSplit = name.split("/"); + + boolean pathIsLong = name.length() >= MAX_PATH_LENGTH; + int length = nameSplit.length; + for(int i=0;i=depth)){ + split = createUniqueName(name); + appendPathName(builder, split); + break; + } + if(fixedDepth && i>=(depth-1)){ + if(i < length-1){ + split = createUniqueName(name); + } + appendPathName(builder, split); + break; + } + appendPathName(builder, split); + } + return builder.toString(); + } + + public void setApkLogger(APKLogger apkLogger) { + this.apkLogger = apkLogger; + } + private String getLogTag(){ + return "[SANITIZE]: "; + } + private void logMessage(String msg){ + APKLogger logger = this.apkLogger; + if(logger!=null){ + logger.logMessage(getLogTag()+msg); + } + } + private void logVerbose(String msg){ + APKLogger logger = this.apkLogger; + if(logger!=null){ + logger.logVerbose(getLogTag()+msg); + } + } + + private static void appendPathName(StringBuilder builder, String name){ + if(builder.length()>0){ + builder.append('/'); + } + builder.append(name); + } + private static String createUniqueName(String name){ + int hash = name.hashCode(); + return String.format("alias_%08x", hash).toLowerCase(); + } + private static boolean isGoodSimpleName(String name){ + if(name==null){ + return false; + } + String alias = sanitizeSimpleName(name); + return name.equals(alias); + } + public static String sanitizeSimpleName(String name){ + if(name==null){ + return null; + } + StringBuilder builder = new StringBuilder(); + char[] chars = name.toCharArray(); + boolean skipNext = true; + int length = 0; + int lengthMax = MAX_NAME_LENGTH; + for(int i=0;i=lengthMax){ + break; + } + char ch = chars[i]; + if(isGoodFileNameSymbol(ch)){ + if(!skipNext){ + builder.append(ch); + length++; + } + skipNext=true; + continue; + } + if(!isGoodFileNameChar(ch)){ + skipNext = true; + continue; + } + builder.append(ch); + length++; + skipNext=false; + } + if(length==0){ + return null; + } + return builder.toString(); + } + + private static boolean isGoodFileNameSymbol(char ch){ + return ch == '.' + || ch == '+' + || ch == '-' + || ch == '_' + || ch == '#'; + } + private static boolean isGoodFileNameChar(char ch){ + return (ch >= '0' && ch <= '9') + || (ch >= 'A' && ch <= 'Z') + || (ch >= 'a' && ch <= 'z'); + } + + private static final int MAX_NAME_LENGTH = 50; + private static final int MAX_PATH_LENGTH = 100; +} diff --git a/src/main/java/com/reandroid/apk/ResFile.java b/src/main/java/com/reandroid/apk/ResFile.java index 71e555f..ff4b337 100644 --- a/src/main/java/com/reandroid/apk/ResFile.java +++ b/src/main/java/com/reandroid/apk/ResFile.java @@ -135,14 +135,23 @@ public class ResFile { return new File(dir, path); } public String buildPath(){ - Entry entry =pickOne(); - TypeBlock typeBlock= entry.getTypeBlock(); - StringBuilder builder=new StringBuilder(); + return buildPath(null); + } + public String buildPath(String parent){ + Entry entry = pickOne(); + StringBuilder builder = new StringBuilder(); + if(parent!=null){ + builder.append(parent); + if(!parent.endsWith("/")){ + builder.append('/'); + } + } + TypeBlock typeBlock = entry.getTypeBlock(); builder.append(typeBlock.getTypeName()); builder.append(typeBlock.getQualifiers()); builder.append('/'); builder.append(entry.getName()); - String ext=getFileExtension(); + String ext = getFileExtension(); if(ext!=null){ builder.append(ext); }