[XML,JSON] keep original apk file paths, fixes: REAndroid/APKEditor#15

This commit is contained in:
REAndroid 2023-03-22 15:12:44 -04:00
parent c650910b2b
commit 43f299cc79
7 changed files with 479 additions and 30 deletions

View File

@ -39,6 +39,10 @@ public class ApkJsonDecoder {
public ApkJsonDecoder(ApkModule apkModule){ public ApkJsonDecoder(ApkModule apkModule){
this(apkModule, false); this(apkModule, false);
} }
public void sanitizeFilePaths(){
PathSanitizer sanitizer = new PathSanitizer(apkModule);
sanitizer.sanitize();
}
public File writeToDirectory(File dir) throws IOException { public File writeToDirectory(File dir) throws IOException {
this.decodedPaths.clear(); this.decodedPaths.clear();
writeUncompressed(dir); writeUncompressed(dir);
@ -48,8 +52,15 @@ public class ApkJsonDecoder {
//writePublicXml(dir); //writePublicXml(dir);
writeResources(dir); writeResources(dir);
writeRootFiles(dir); writeRootFiles(dir);
writePathMap(dir);
return new File(dir, apkModule.getModuleName()); 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 { private void writeUncompressed(File dir) throws IOException {
File file=toUncompressedJsonFile(dir); File file=toUncompressedJsonFile(dir);
UncompressedFiles uncompressedFiles=new UncompressedFiles(); UncompressedFiles uncompressedFiles=new UncompressedFiles();
@ -77,17 +88,9 @@ public class ApkJsonDecoder {
jsonObject.write(file); jsonObject.write(file);
addDecoded(path); addDecoded(path);
} }
// TODO: temporary fix
private void writeRootFiles(File dir) throws IOException { private void writeRootFiles(File dir) throws IOException {
for(InputSource inputSource:apkModule.getApkArchive().listInputSources()){ for(InputSource inputSource:apkModule.getApkArchive().listInputSources()){
try{
writeRootFile(dir, inputSource); writeRootFile(dir, inputSource);
}catch (IOException ex){
APKLogger logger = apkModule.getApkLogger();
if(logger!=null){
logger.logMessage("ERROR: "+ex.getMessage());
}
}
} }
} }
private void writeRootFile(File dir, InputSource inputSource) throws IOException { private void writeRootFile(File dir, InputSource inputSource) throws IOException {
@ -186,6 +189,10 @@ public class ApkJsonDecoder {
String name = "public.xml"; String name = "public.xml";
return new File(file, name); 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){ private File toUncompressedJsonFile(File dir){
File file = new File(dir, apkModule.getModuleName()); File file = new File(dir, apkModule.getModuleName());
return new File(file, UncompressedFiles.JSON_FILE); return new File(file, UncompressedFiles.JSON_FILE);

View File

@ -19,8 +19,11 @@ import com.reandroid.archive.APKArchive;
import com.reandroid.archive.FileInputSource; import com.reandroid.archive.FileInputSource;
import com.reandroid.arsc.chunk.TableBlock; import com.reandroid.arsc.chunk.TableBlock;
import com.reandroid.arsc.chunk.xml.AndroidManifestBlock; import com.reandroid.arsc.chunk.xml.AndroidManifestBlock;
import com.reandroid.json.JSONArray;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
@ -40,8 +43,27 @@ public class ApkJsonEncoder {
module.setAPKLogger(apkLogger); module.setAPKLogger(apkLogger);
loadUncompressed(module, moduleDir); loadUncompressed(module, moduleDir);
applyResourceId(module, moduleDir); applyResourceId(module, moduleDir);
restorePathMap(moduleDir, module);
return 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) { private void applyResourceId(ApkModule apkModule, File moduleDir) {
if(!apkModule.hasTableBlock()){ if(!apkModule.hasTableBlock()){
return; return;

View File

@ -48,6 +48,10 @@ import java.util.*;
this.decodedEntries = new HashMap<>(); this.decodedEntries = new HashMap<>();
this.mDecodedPaths = new HashSet<>(); this.mDecodedPaths = new HashSet<>();
} }
public void sanitizeFilePaths(){
PathSanitizer sanitizer = new PathSanitizer(apkModule);
sanitizer.sanitize();
}
public void decodeTo(File outDir) public void decodeTo(File outDir)
throws IOException, XMLException { throws IOException, XMLException {
this.decodedEntries.clear(); this.decodedEntries.clear();
@ -74,6 +78,14 @@ import java.util.*;
decodeValues(tableBlock, outDir, tableBlock); decodeValues(tableBlock, outDir, tableBlock);
extractRootFiles(outDir); 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 { private void decodePackageInfo(File outDir, TableBlock tableBlock) throws IOException {
for(PackageBlock packageBlock:tableBlock.listPackages()){ for(PackageBlock packageBlock:tableBlock.listPackages()){
@ -107,18 +119,17 @@ import java.util.*;
PackageBlock packageBlock= entry.getPackageBlock(); PackageBlock packageBlock= entry.getPackageBlock();
File pkgDir=new File(outDir, getPackageDirName(packageBlock)); File pkgDir=new File(outDir, getPackageDirName(packageBlock));
File resDir=new File(pkgDir, ApkUtil.RES_DIR_NAME); String alias = resFile.buildPath(ApkUtil.RES_DIR_NAME);
String path=resFile.buildPath(); String path = alias.replace('/', File.separatorChar);
path=path.replace('/', File.separatorChar); File file=new File(pkgDir, path);
File file=new File(resDir, path);
File dir=file.getParentFile(); File dir=file.getParentFile();
if(!dir.exists()){ if(!dir.exists()){
dir.mkdirs(); dir.mkdirs();
} }
FileOutputStream outputStream=new FileOutputStream(file); FileOutputStream outputStream=new FileOutputStream(file);
resFile.getInputSource().write(outputStream); resFile.getInputSource().write(outputStream);
outputStream.close(); outputStream.close();
resFile.setFilePath(alias);
addDecodedEntry(entry); addDecodedEntry(entry);
} }
@ -130,10 +141,10 @@ import java.util.*;
resFile.getInputSource().getName()); resFile.getInputSource().getName());
File pkgDir=new File(outDir, getPackageDirName(packageBlock)); File pkgDir=new File(outDir, getPackageDirName(packageBlock));
File resDir=new File(pkgDir, ApkUtil.RES_DIR_NAME); String alias = resFile.buildPath(ApkUtil.RES_DIR_NAME);
String path=resFile.buildPath(); String path = alias.replace('/', File.separatorChar);
path=path.replace('/', File.separatorChar); path=path.replace('/', File.separatorChar);
File file=new File(resDir, path); File file=new File(pkgDir, path);
logVerbose("Decoding: "+path); logVerbose("Decoding: "+path);
XMLNamespaceValidator namespaceValidator=new XMLNamespaceValidator(resXmlDocument); XMLNamespaceValidator namespaceValidator=new XMLNamespaceValidator(resXmlDocument);
@ -141,6 +152,8 @@ import java.util.*;
XMLDocument xmlDocument= resXmlDocument.decodeToXml(entryStore, packageBlock.getId()); XMLDocument xmlDocument= resXmlDocument.decodeToXml(entryStore, packageBlock.getId());
xmlDocument.save(file, true); xmlDocument.save(file, true);
resFile.setFilePath(alias);
addDecodedEntry(resFile.pickOne()); addDecodedEntry(resFile.pickOne());
} }
private void decodePublicXml(TableBlock tableBlock, File outDir) private void decodePublicXml(TableBlock tableBlock, File outDir)
@ -304,14 +317,9 @@ import java.util.*;
if(!dir.exists()){ if(!dir.exists()){
dir.mkdirs(); dir.mkdirs();
} }
// TODO:Temporary fix
try{
FileOutputStream outputStream=new FileOutputStream(file); FileOutputStream outputStream=new FileOutputStream(file);
inputSource.write(outputStream); inputSource.write(outputStream);
outputStream.close(); outputStream.close();
}catch (IOException ex){
logMessage("ERROR: "+ex.getMessage());
}
} }
private boolean containsDecodedPath(String path){ private boolean containsDecodedPath(String path){
return mDecodedPaths.contains(path); return mDecodedPaths.contains(path);

View File

@ -19,9 +19,11 @@ package com.reandroid.apk;
import com.reandroid.archive.FileInputSource; import com.reandroid.archive.FileInputSource;
import com.reandroid.apk.xmlencoder.RESEncoder; import com.reandroid.apk.xmlencoder.RESEncoder;
import com.reandroid.arsc.chunk.TableBlock; import com.reandroid.arsc.chunk.TableBlock;
import com.reandroid.json.JSONArray;
import com.reandroid.xml.XMLException; import com.reandroid.xml.XMLException;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
@ -38,6 +40,18 @@ package com.reandroid.apk;
resEncoder.scanDirectory(mainDirectory); resEncoder.scanDirectory(mainDirectory);
File rootDir=new File(mainDirectory, "root"); File rootDir=new File(mainDirectory, "root");
scanRootDir(rootDir); 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(){ public ApkModule getApkModule(){
return resEncoder.getApkModule(); return resEncoder.getApkModule();

View File

@ -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<JSONArray> {
private final Object mLock = new Object();
private final Map<String, String> mNameAliasMap;
private final Map<String, String> 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<String> restoreResFile(Collection<ResFile> files){
List<String> 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<String> restore(Collection<InputSource> sources){
List<String> 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<InputSource> 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<String, String> nameMap = this.mNameAliasMap;
List<String> 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<length;i++){
add(json.optJSONObject(i));
}
}
@Override
public String toString(){
return "PathMap size="+size();
}
private static List<String> toSortedList(Collection<String> stringCollection){
List<String> results;
if(stringCollection instanceof List){
results = (List<String>) stringCollection;
}else {
results = new ArrayList<>(stringCollection);
}
Comparator<String> cmp = new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
};
results.sort(cmp);
return results;
}
public static final String NAME_name = "name";
public static final String NAME_alias = "alias";
public static final String JSON_FILE = "path-map.json";
}

View File

@ -0,0 +1,183 @@
/*
* 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 java.util.HashSet;
import java.util.List;
import java.util.Set;
public class PathSanitizer {
private final ApkModule apkModule;
private APKLogger apkLogger;
private final Set<String> 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<ResFile> resFileList = apkModule.listResFiles();
for(ResFile resFile:resFileList){
sanitize(resFile);
}
List<InputSource> 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<length;i++){
String split = nameSplit[i];
boolean good = isGoodSimpleName(split);
if(!good || (pathIsLong && 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<chars.length;i++){
if(length>=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;
}

View File

@ -135,9 +135,18 @@ public class ResFile {
return new File(dir, path); return new File(dir, path);
} }
public String buildPath(){ public String buildPath(){
return buildPath(null);
}
public String buildPath(String parent){
Entry entry = pickOne(); Entry entry = pickOne();
TypeBlock typeBlock= entry.getTypeBlock();
StringBuilder builder = new StringBuilder(); 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.getTypeName());
builder.append(typeBlock.getQualifiers()); builder.append(typeBlock.getQualifiers());
builder.append('/'); builder.append('/');