replace arsc_parser with ARSClib

This commit is contained in:
Julian Winkler
2023-05-20 12:34:21 +02:00
parent 0baddd9fe8
commit db53d3679f
393 changed files with 58984 additions and 1715 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,65 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* 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.android.org.kxml2.io;
// Taken from libcore.internal.StringPool
class LibCoreStringPool {
private final String[] pool = new String[512];
public LibCoreStringPool() {
}
private static boolean contentEquals(String s, char[] chars, int start, int length) {
if (s.length() != length) {
return false;
}
for (int i = 0; i < length; i++) {
if (chars[start + i] != s.charAt(i)) {
return false;
}
}
return true;
}
/**
* Returns a string equal to {@code new String(array, start, length)}.
*/
public String get(char[] array, int start, int length) {
// Compute an arbitrary hash of the content
int hashCode = 0;
for (int i = start; i < start + length; i++) {
hashCode = (hashCode * 31) + array[i];
}
// Pick a bucket using Doug Lea's supplemental secondaryHash function (from HashMap)
hashCode ^= (hashCode >>> 20) ^ (hashCode >>> 12);
hashCode ^= (hashCode >>> 7) ^ (hashCode >>> 4);
int index = hashCode & (pool.length - 1);
String pooled = pool[index];
if (pooled != null && contentEquals(pooled, array, start, length)) {
return pooled;
}
String result = new String(array, start, length);
pool[index] = result;
return result;
}
}

View File

@@ -0,0 +1,22 @@
/*
* 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;
public interface APKLogger {
void logMessage(String msg);
void logError(String msg, Throwable tr);
void logVerbose(String msg);
}

View File

@@ -0,0 +1,219 @@
/*
* 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 java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
public class AndroidFrameworks {
private static Map<Integer, String> resource_paths;
private static FrameworkApk mCurrent;
public static void setCurrent(FrameworkApk current){
synchronized (AndroidFrameworks.class){
mCurrent = current;
}
}
public static FrameworkApk getCurrent(){
FrameworkApk current = mCurrent;
if(current==null){
return null;
}
if(current.isDestroyed()){
mCurrent = null;
return null;
}
return current;
}
public static FrameworkApk getLatest() throws IOException {
Map<Integer, String> pathMap = getResourcePaths();
synchronized (AndroidFrameworks.class){
int latest = getHighestVersion();
FrameworkApk current = getCurrent();
if(current!=null && latest==current.getVersionCode()){
return current;
}
String path = pathMap.get(latest);
if(path == null){
throw new IOException("Could not get latest framework");
}
return loadResource(latest);
}
}
public static FrameworkApk getBestMatch(int version) throws IOException {
Map<Integer, String> pathMap = getResourcePaths();
synchronized (AndroidFrameworks.class){
int best = getBestMatchVersion(version);
FrameworkApk current = getCurrent();
if(current!=null && best==current.getVersionCode()){
return current;
}
String path = pathMap.get(best);
if(path == null){
throw new IOException("Could not get framework for version = "+version);
}
return loadResource(best);
}
}
public static void destroyCurrent(){
synchronized (AndroidFrameworks.class){
FrameworkApk current = mCurrent;
if(current==null){
return;
}
current.destroy();
}
}
private static int getHighestVersion() {
Map<Integer, String> pathMap = getResourcePaths();
int highest = 0;
for(int id:pathMap.keySet()){
if(highest==0){
highest = id;
continue;
}
if(id>highest){
highest = id;
}
}
return highest;
}
private static int getBestMatchVersion(int version) {
Map<Integer, String> pathMap = getResourcePaths();
if(pathMap.containsKey(version)){
return version;
}
int highest = 0;
int best = 0;
int prevDifference = 0;
for(int id:pathMap.keySet()){
if(highest==0){
highest = id;
best = id;
prevDifference = version*2 + 1000;
continue;
}
if(id>highest){
highest = id;
}
int diff = id-version;
if(diff<0){
diff=-diff;
}
if(diff<prevDifference || (diff==prevDifference && id>best)){
best = id;
prevDifference = diff;
}
}
return best;
}
public static FrameworkApk loadResource(int version) throws IOException {
String path = getResourcePath(version);
if(path == null){
throw new IOException("No resource found for version: "+version);
}
String simpleName = toSimpleName(path);
return FrameworkApk.loadApkBuffer(simpleName, AndroidFrameworks.class.getResourceAsStream(path));
}
private static String getResourcePath(int version){
return getResourcePaths().get(version);
}
private static Map<Integer, String> getResourcePaths(){
if(resource_paths!=null){
return resource_paths;
}
synchronized (AndroidFrameworks.class){
resource_paths = scanAvailableResourcePaths();
return resource_paths;
}
}
private static Map<Integer, String> scanAvailableResourcePaths(){
Map<Integer, String> results = new HashMap<>();
int maxSearch = 50;
for(int version=20; version<maxSearch; version++){
String path = toResourcePath(version);
if(!isAvailable(path)){
continue;
}
results.put(version, path);
maxSearch = version + 20;
}
return results;
}
private static String toSimpleName(String path){
int i = path.lastIndexOf('/');
if(i<0){
i = path.lastIndexOf(File.separatorChar);
}
if(i>0){
i++;
path = path.substring(i);
}
i = path.lastIndexOf('.');
if(i>=0){
path = path.substring(0, i);
}
return path;
}
private static int parseVersion(String name){
int i = name.lastIndexOf('/');
if(i<0){
i = name.lastIndexOf(File.separatorChar);
}
if(i>0){
i++;
name = name.substring(i);
}
i = name.lastIndexOf('-');
if(i>=0){
i++;
name = name.substring(i);
}
i = name.indexOf('.');
if(i>=0){
name = name.substring(0, i);
}
return Integer.parseInt(name);
}
private static boolean isAvailable(String path){
InputStream inputStream = AndroidFrameworks.class.getResourceAsStream(path);
if(inputStream==null){
return false;
}
closeQuietly(inputStream);
return true;
}
private static void closeQuietly(InputStream stream){
if(stream == null){
return;
}
try {
stream.close();
} catch (IOException ignored) {
}
}
private static String toResourcePath(int version){
return ANDROID_RESOURCE_DIRECTORY + ANDROID_PACKAGE
+ '-' + version
+FRAMEWORK_EXTENSION;
}
private static final String ANDROID_RESOURCE_DIRECTORY = "/frameworks/android/";
private static final String ANDROID_PACKAGE = "android";
private static final String FRAMEWORK_EXTENSION = ".apk";
}

View File

@@ -0,0 +1,216 @@
/*
* 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.APKArchive;
import com.reandroid.archive2.block.ApkSignatureBlock;
import com.reandroid.arsc.chunk.TableBlock;
import com.reandroid.arsc.pool.TableStringPool;
import com.reandroid.arsc.pool.builder.StringPoolMerger;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.*;
public class ApkBundle {
private final Map<String, ApkModule> mModulesMap;
private APKLogger apkLogger;
public ApkBundle(){
this.mModulesMap=new HashMap<>();
}
public ApkModule mergeModules() throws IOException {
List<ApkModule> moduleList=getApkModuleList();
if(moduleList.size()==0){
throw new FileNotFoundException("Nothing to merge, empty modules");
}
ApkModule result = new ApkModule(generateMergedModuleName(), new APKArchive());
result.setAPKLogger(apkLogger);
result.setLoadDefaultFramework(false);
mergeStringPools(result);
ApkModule base=getBaseModule();
if(base==null){
base=getLargestTableModule();
}
result.merge(base);
ApkSignatureBlock signatureBlock = null;
for(ApkModule module:moduleList){
ApkSignatureBlock asb = module.getApkSignatureBlock();
if(module==base){
if(asb != null){
signatureBlock = asb;
}
continue;
}
if(signatureBlock == null){
signatureBlock = asb;
}
result.merge(module);
}
result.setApkSignatureBlock(signatureBlock);
if(result.hasTableBlock()){
TableBlock tableBlock=result.getTableBlock();
tableBlock.sortPackages();
tableBlock.refresh();
}
result.getApkArchive().autoSortApkFiles();
return result;
}
private void mergeStringPools(ApkModule mergedModule) throws IOException {
if(!hasOneTableBlock() || mergedModule.hasTableBlock()){
return;
}
logMessage("Merging string pools ... ");
TableBlock createdTable = new TableBlock();
BlockInputSource<TableBlock> inputSource=
new BlockInputSource<>(TableBlock.FILE_NAME, createdTable);
mergedModule.getApkArchive().add(inputSource);
StringPoolMerger poolMerger = new StringPoolMerger();
for(ApkModule apkModule:getModules()){
if(!apkModule.hasTableBlock()){
continue;
}
TableStringPool stringPool = apkModule.getVolatileTableStringPool();
poolMerger.add(stringPool);
}
poolMerger.mergeTo(createdTable.getTableStringPool());
logMessage("Merged string pools="+poolMerger.getMergedPools()
+", style="+poolMerger.getMergedStyleStrings()
+", strings="+poolMerger.getMergedStrings());
}
private String generateMergedModuleName(){
Set<String> moduleNames=mModulesMap.keySet();
String merged="merged";
int i=1;
String name=merged;
while (moduleNames.contains(name)){
name=merged+"_"+i;
i++;
}
return name;
}
private ApkModule getLargestTableModule(){
ApkModule apkModule=null;
int chunkSize=0;
for(ApkModule module:getApkModuleList()){
if(!module.hasTableBlock()){
continue;
}
TableBlock tableBlock=module.getTableBlock();
int size=tableBlock.getHeaderBlock().getChunkSize();
if(apkModule==null || size>chunkSize){
chunkSize=size;
apkModule=module;
}
}
return apkModule;
}
public ApkModule getBaseModule(){
for(ApkModule module:getApkModuleList()){
if(module.isBaseModule()){
return module;
}
}
return null;
}
public List<ApkModule> getApkModuleList(){
return new ArrayList<>(mModulesMap.values());
}
public void loadApkDirectory(File dir) throws IOException{
loadApkDirectory(dir, false);
}
public void loadApkDirectory(File dir, boolean recursive) throws IOException {
if(!dir.isDirectory()){
throw new FileNotFoundException("No such directory: "+dir);
}
List<File> apkList;
if(recursive){
apkList = ApkUtil.recursiveFiles(dir, ".apk");
}else {
apkList = ApkUtil.listFiles(dir, ".apk");
}
if(apkList.size()==0){
throw new FileNotFoundException("No '*.apk' files in directory: "+dir);
}
logMessage("Found apk files: "+apkList.size());
for(File file:apkList){
logVerbose("Loading: "+file.getName());
String name = ApkUtil.toModuleName(file);
ApkModule module = ApkModule.loadApkFile(file, name);
module.setAPKLogger(apkLogger);
addModule(module);
}
}
public void addModule(ApkModule apkModule){
apkModule.setLoadDefaultFramework(false);
String name = apkModule.getModuleName();
mModulesMap.remove(name);
mModulesMap.put(name, apkModule);
}
public boolean containsApkModule(String moduleName){
return mModulesMap.containsKey(moduleName);
}
public ApkModule removeApkModule(String moduleName){
return mModulesMap.remove(moduleName);
}
public ApkModule getApkModule(String moduleName){
return mModulesMap.get(moduleName);
}
public List<String> listModuleNames(){
return new ArrayList<>(mModulesMap.keySet());
}
public int countModules(){
return mModulesMap.size();
}
public Collection<ApkModule> getModules(){
return mModulesMap.values();
}
private boolean hasOneTableBlock(){
for(ApkModule apkModule:getModules()){
if(apkModule.hasTableBlock()){
return true;
}
}
return false;
}
public void setAPKLogger(APKLogger logger) {
this.apkLogger = logger;
}
private void logMessage(String msg) {
if(apkLogger!=null){
apkLogger.logMessage(msg);
}
}
private void logError(String msg, Throwable tr) {
if(apkLogger!=null){
apkLogger.logError(msg, tr);
}
}
private void logVerbose(String msg) {
if(apkLogger!=null){
apkLogger.logVerbose(msg);
}
}
}

View File

@@ -0,0 +1,110 @@
/*
* 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.archive2.block.ApkSignatureBlock;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
public abstract class ApkDecoder {
private final Set<String> mDecodedPaths;
private APKLogger apkLogger;
private boolean mLogErrors;
public ApkDecoder(){
mDecodedPaths = new HashSet<>();
}
public final void decodeTo(File outDir) throws IOException{
reset();
onDecodeTo(outDir);
}
abstract void onDecodeTo(File outDir) throws IOException;
boolean containsDecodedPath(String path){
return mDecodedPaths.contains(path);
}
void addDecodedPath(String path){
mDecodedPaths.add(path);
}
void writePathMap(File dir, Collection<? extends InputSource> sourceList) throws IOException {
PathMap pathMap = new PathMap();
pathMap.add(sourceList);
File file = new File(dir, PathMap.JSON_FILE);
pathMap.toJson().write(file);
}
void dumpSignatures(File outDir, ApkSignatureBlock signatureBlock) throws IOException {
if(signatureBlock == null){
return;
}
logMessage("Dumping signatures ...");
File dir = new File(outDir, ApkUtil.SIGNATURE_DIR_NAME);
signatureBlock.writeSplitRawToDirectory(dir);
}
void logOrThrow(String message, IOException exception) throws IOException{
if(isLogErrors()){
logError(message, exception);
return;
}
if(message == null && exception == null){
return;
}
if(exception == null){
exception = new IOException(message);
}
throw exception;
}
private void reset(){
mDecodedPaths.clear();
}
public boolean isLogErrors() {
return mLogErrors;
}
public void setLogErrors(boolean logErrors) {
this.mLogErrors = logErrors;
}
public void setApkLogger(APKLogger apkLogger) {
this.apkLogger = apkLogger;
}
APKLogger getApkLogger() {
return 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);
}
}
}

View File

@@ -0,0 +1,233 @@
/*
* 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.archive2.block.ApkSignatureBlock;
import com.reandroid.arsc.chunk.TableBlock;
import com.reandroid.arsc.chunk.xml.AndroidManifestBlock;
import com.reandroid.arsc.chunk.xml.ResXmlDocument;
import com.reandroid.json.JSONObject;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
public class ApkJsonDecoder {
private final ApkModule apkModule;
private final Set<String> decodedPaths;
private final boolean splitTypes;
public ApkJsonDecoder(ApkModule apkModule, boolean splitTypes){
this.apkModule = apkModule;
this.splitTypes = splitTypes;
this.decodedPaths = new HashSet<>();
}
public ApkJsonDecoder(ApkModule apkModule){
this(apkModule, false);
}
public void sanitizeFilePaths(){
PathSanitizer sanitizer = PathSanitizer.create(apkModule);
sanitizer.sanitize();
}
public File writeToDirectory(File dir) throws IOException {
this.decodedPaths.clear();
writeUncompressed(dir);
writeManifest(dir);
writeTable(dir);
//writeResourceIds(dir);
//writePublicXml(dir);
writeResources(dir);
writeRootFiles(dir);
writePathMap(dir);
dumpSignatures(dir);
return new File(dir, apkModule.getModuleName());
}
private void dumpSignatures(File outDir) throws IOException {
ApkSignatureBlock signatureBlock = apkModule.getApkSignatureBlock();
if(signatureBlock == null){
return;
}
apkModule.logMessage("Dumping signatures ...");
File dir = toSignatureDir(outDir);
signatureBlock.writeSplitRawToDirectory(dir);
}
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();
uncompressedFiles.addCommonExtensions();
uncompressedFiles.addPath(apkModule.getApkArchive());
uncompressedFiles.toJson().write(file);
}
private void writeResources(File dir) throws IOException {
for(ResFile resFile:apkModule.listResFiles()){
writeResource(dir, resFile);
}
}
private void writeResource(File dir, ResFile resFile) throws IOException {
if(resFile.isBinaryXml()){
writeResourceJson(dir, resFile);
}
}
private void writeResourceJson(File dir, ResFile resFile) throws IOException {
InputSource inputSource= resFile.getInputSource();
String path=inputSource.getAlias();
File file=toResJson(dir, path);
ResXmlDocument resXmlDocument =new ResXmlDocument();
resXmlDocument.readBytes(inputSource.openStream());
JSONObject jsonObject= resXmlDocument.toJson();
jsonObject.write(file);
addDecoded(path);
}
private void writeRootFiles(File dir) throws IOException {
for(InputSource inputSource:apkModule.getApkArchive().listInputSources()){
writeRootFile(dir, inputSource);
}
}
private void writeRootFile(File dir, InputSource inputSource) throws IOException {
String path=inputSource.getAlias();
if(hasDecoded(path)){
return;
}
File file=toRootFile(dir, path);
File parent=file.getParentFile();
if(parent!=null && !parent.exists()){
parent.mkdirs();
}
FileOutputStream outputStream=new FileOutputStream(file);
inputSource.write(outputStream);
outputStream.close();
addDecoded(path);
}
private void writeTable(File dir) throws IOException {
if(!splitTypes){
writeTableSingle(dir);
return;
}
writeTableSplit(dir);
}
private void writeTableSplit(File dir) throws IOException {
if(!apkModule.hasTableBlock()){
return;
}
TableBlock tableBlock = apkModule.getTableBlock();
File splitDir= toJsonTableSplitDir(dir);
TableBlockJson tableBlockJson=new TableBlockJson(tableBlock);
tableBlockJson.writeJsonFiles(splitDir);
addDecoded(TableBlock.FILE_NAME);
}
private void writeTableSingle(File dir) throws IOException {
if(!apkModule.hasTableBlock()){
return;
}
TableBlock tableBlock = apkModule.getTableBlock();
File file= toJsonTableFile(dir);
tableBlock.toJson().write(file);
addDecoded(TableBlock.FILE_NAME);
}
private void writeResourceIds(File dir) throws IOException {
if(!apkModule.hasTableBlock()){
return;
}
TableBlock tableBlock = apkModule.getTableBlock();
ResourceIds resourceIds=new ResourceIds();
resourceIds.loadTableBlock(tableBlock);
JSONObject jsonObject= resourceIds.toJson();
File file=toResourceIds(dir);
jsonObject.write(file);
}
private void writePublicXml(File dir) throws IOException {
if(!apkModule.hasTableBlock()){
return;
}
TableBlock tableBlock = apkModule.getTableBlock();
ResourceIds resourceIds=new ResourceIds();
resourceIds.loadTableBlock(tableBlock);
File file=toResourceIdsXml(dir);
resourceIds.writeXml(file);
}
private void writeManifest(File dir) throws IOException {
if(!apkModule.hasAndroidManifestBlock()){
return;
}
AndroidManifestBlock manifestBlock = apkModule.getAndroidManifestBlock();
File file = toJsonManifestFile(dir);
manifestBlock.toJson().write(file);
addDecoded(AndroidManifestBlock.FILE_NAME);
}
private boolean hasDecoded(String path){
return decodedPaths.contains(path);
}
private void addDecoded(String path){
this.decodedPaths.add(path);
}
private File toJsonTableFile(File dir){
File file=new File(dir, apkModule.getModuleName());
String name = TableBlock.FILE_NAME + ApkUtil.JSON_FILE_EXTENSION;
return new File(file, name);
}
private File toJsonTableSplitDir(File dir){
File file=new File(dir, apkModule.getModuleName());
return new File(file, ApkUtil.SPLIT_JSON_DIRECTORY);
}
private File toResourceIds(File dir){
File file=new File(dir, apkModule.getModuleName());
String name = "resource-ids.json";
return new File(file, name);
}
private File toResourceIdsXml(File dir){
File file=new File(dir, apkModule.getModuleName());
String name = "public.xml";
return new File(file, name);
}
private File toSignatureDir(File dir){
dir = new File(dir, apkModule.getModuleName());
return new File(dir, ApkUtil.SIGNATURE_DIR_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);
}
private File toJsonManifestFile(File dir){
File file=new File(dir, apkModule.getModuleName());
String name = AndroidManifestBlock.FILE_NAME + ApkUtil.JSON_FILE_EXTENSION;
return new File(file, name);
}
private File toResJson(File dir, String path){
File file=new File(dir, apkModule.getModuleName());
file=new File(file, ApkUtil.RES_JSON_NAME);
path=path + ApkUtil.JSON_FILE_EXTENSION;
path=path.replace('/', File.separatorChar);
return new File(file, path);
}
private File toRootFile(File dir, String path){
File file=new File(dir, apkModule.getModuleName());
file=new File(file, ApkUtil.ROOT_NAME);
path=path.replace('/', File.separatorChar);
return new File(file, path);
}
}

View File

@@ -0,0 +1,209 @@
/*
* 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.APKArchive;
import com.reandroid.archive.FileInputSource;
import com.reandroid.archive2.block.ApkSignatureBlock;
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;
public class ApkJsonEncoder {
private APKArchive apkArchive;
private APKLogger apkLogger;
public ApkJsonEncoder(){
}
public ApkModule scanDirectory(File moduleDir){
this.apkArchive=new APKArchive();
String moduleName=moduleDir.getName();
scanManifest(moduleDir);
scanTable(moduleDir);
scanResJsonDirs(moduleDir);
scanRootDirs(moduleDir);
ApkModule module=new ApkModule(moduleName, apkArchive);
module.setLoadDefaultFramework(false);
module.setAPKLogger(apkLogger);
loadUncompressed(module, moduleDir);
//applyResourceId(module, moduleDir);
restorePathMap(moduleDir, module);
restoreSignatures(moduleDir, module);
return module;
}
private void restoreSignatures(File dir, ApkModule apkModule){
File sigDir = new File(dir, ApkUtil.SIGNATURE_DIR_NAME);
if(!sigDir.isDirectory()){
return;
}
logMessage("Loading signatures ...");
ApkSignatureBlock signatureBlock = new ApkSignatureBlock();
try {
signatureBlock.scanSplitFiles(sigDir);
apkModule.setApkSignatureBlock(signatureBlock);
} catch (IOException exception){
logError("Failed to load signatures: ", exception);
}
}
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;
}
File pubXml=toResourceIdsXml(moduleDir);
if(!pubXml.isFile()){
return;
}
ResourceIds resourceIds=new ResourceIds();
try {
resourceIds.fromXml(pubXml);
resourceIds.applyTo(apkModule.getTableBlock());
} catch (IOException exception) {
throw new IllegalArgumentException(exception.getMessage());
}
}
private void loadUncompressed(ApkModule module, File moduleDir){
File jsonFile=toUncompressedJsonFile(moduleDir);
UncompressedFiles uf= module.getUncompressedFiles();
try {
uf.fromJson(jsonFile);
} catch (IOException ignored) {
}
}
private void scanRootDirs(File moduleDir){
File rootDir=toRootDir(moduleDir);
List<File> jsonFileList=ApkUtil.recursiveFiles(rootDir);
for(File file:jsonFileList){
scanRootFile(rootDir, file);
}
}
private void scanRootFile(File rootDir, File file){
String path=ApkUtil.toArchivePath(rootDir, file);
FileInputSource inputSource=new FileInputSource(file, path);
apkArchive.add(inputSource);
}
private void scanResJsonDirs(File moduleDir){
File resJsonDir=toResJsonDir(moduleDir);
List<File> jsonFileList=ApkUtil.recursiveFiles(resJsonDir);
for(File file:jsonFileList){
scanResJsonFile(resJsonDir, file);
}
}
private void scanResJsonFile(File resJsonDir, File file){
JsonXmlInputSource inputSource=JsonXmlInputSource.fromFile(resJsonDir, file);
apkArchive.add(inputSource);
}
private void scanManifest(File moduleDir){
File file=toJsonManifestFile(moduleDir);
if(!file.isFile()){
return;
}
JsonManifestInputSource inputSource=JsonManifestInputSource.fromFile(moduleDir, file);
inputSource.setAPKLogger(apkLogger);
apkArchive.add(inputSource);
}
private void scanTable(File moduleDir) {
boolean splitFound=scanTableSplitJson(moduleDir);
if(splitFound){
return;
}
scanTableSingleJson(moduleDir);
}
private boolean scanTableSplitJson(File moduleDir) {
File dir=toJsonTableSplitDir(moduleDir);
if(!dir.isDirectory()){
return false;
}
SplitJsonTableInputSource inputSource=new SplitJsonTableInputSource(dir);
inputSource.setAPKLogger(apkLogger);
apkArchive.add(inputSource);
return true;
}
private void scanTableSingleJson(File moduleDir) {
File file=toJsonTableFile(moduleDir);
if(!file.isFile()){
return;
}
SingleJsonTableInputSource inputSource= SingleJsonTableInputSource.fromFile(moduleDir, file);
inputSource.setAPKLogger(apkLogger);
apkArchive.add(inputSource);
}
private File toJsonTableFile(File dir){
String name = TableBlock.FILE_NAME + ApkUtil.JSON_FILE_EXTENSION;
return new File(dir, name);
}
private File toJsonManifestFile(File dir){
String name = AndroidManifestBlock.FILE_NAME + ApkUtil.JSON_FILE_EXTENSION;
return new File(dir, name);
}
private File toResourceIdsXml(File dir){
String name = "public.xml";
return new File(dir, name);
}
private File toUncompressedJsonFile(File dir){
return new File(dir, UncompressedFiles.JSON_FILE);
}
private File toJsonTableSplitDir(File dir){
return new File(dir, ApkUtil.SPLIT_JSON_DIRECTORY);
}
private File toResJsonDir(File dir){
return new File(dir, ApkUtil.RES_JSON_NAME);
}
private File toRootDir(File dir){
return new File(dir, ApkUtil.ROOT_NAME);
}
public void setAPKLogger(APKLogger logger) {
this.apkLogger = logger;
}
private void logMessage(String msg) {
if(apkLogger!=null){
apkLogger.logMessage(msg);
}
}
private void logError(String msg, Throwable tr) {
if(apkLogger!=null){
apkLogger.logError(msg, tr);
}
}
private void logVerbose(String msg) {
if(apkLogger!=null){
apkLogger.logVerbose(msg);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,320 @@
/*
* 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.apk.xmldecoder.*;
import com.reandroid.archive.InputSource;
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.ResXmlDocument;
import com.reandroid.arsc.container.SpecTypePair;
import com.reandroid.arsc.value.*;
import com.reandroid.identifiers.PackageIdentifier;
import com.reandroid.json.JSONObject;
import com.reandroid.xml.XMLDocument;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.*;
import java.util.function.Predicate;
public class ApkModuleXmlDecoder extends ApkDecoder implements Predicate<Entry> {
private final ApkModule apkModule;
private final Map<Integer, Set<ResConfig>> decodedEntries;
private ResXmlDocumentSerializer documentSerializer;
private XMLEntryDecoderSerializer entrySerializer;
public ApkModuleXmlDecoder(ApkModule apkModule){
super();
this.apkModule = apkModule;
this.decodedEntries = new HashMap<>();
super.setApkLogger(apkModule.getApkLogger());
}
public void sanitizeFilePaths(){
PathSanitizer sanitizer = PathSanitizer.create(apkModule);
sanitizer.sanitize();
}
@Override
void onDecodeTo(File outDir) throws IOException{
this.decodedEntries.clear();
logMessage("Decoding ...");
if(!apkModule.hasTableBlock()){
logOrThrow(null, new IOException("Don't have resource table"));
return;
}
decodeUncompressedFiles(outDir);
TableBlock tableBlock = apkModule.getTableBlock();
this.entrySerializer = new XMLEntryDecoderSerializer(tableBlock);
this.entrySerializer.setDecodedEntries(this);
decodeAndroidManifest(outDir, apkModule.getAndroidManifestBlock());
decodeTableBlock(outDir, tableBlock);
logMessage("Decoding resource files ...");
List<ResFile> resFileList = apkModule.listResFiles();
for(ResFile resFile:resFileList){
decodeResFile(outDir, resFile);
}
decodeValues(outDir, tableBlock);
extractRootFiles(outDir);
writePathMap(outDir, apkModule.getApkArchive().listInputSources());
dumpSignatures(outDir, apkModule.getApkSignatureBlock());
}
private void decodeTableBlock(File outDir, TableBlock tableBlock) throws IOException {
try{
decodePackageInfo(outDir, tableBlock);
decodePublicXml(tableBlock, outDir);
addDecodedPath(TableBlock.FILE_NAME);
}catch (IOException exception){
logOrThrow("Error decoding resource table", exception);
}
}
private void decodePackageInfo(File outDir, TableBlock tableBlock) throws IOException {
for(PackageBlock packageBlock:tableBlock.listPackages()){
decodePackageInfo(outDir, packageBlock);
}
}
private void decodePackageInfo(File outDir, PackageBlock packageBlock) throws IOException {
File pkgDir = new File(outDir, getPackageDirName(packageBlock));
File packageJsonFile = new File(pkgDir, PackageBlock.JSON_FILE_NAME);
JSONObject jsonObject = packageBlock.toJson(false);
jsonObject.write(packageJsonFile);
}
private void decodeUncompressedFiles(File outDir)
throws IOException {
File file=new File(outDir, UncompressedFiles.JSON_FILE);
UncompressedFiles uncompressedFiles = apkModule.getUncompressedFiles();
uncompressedFiles.toJson().write(file);
}
private void decodeResFile(File outDir, ResFile resFile)
throws IOException{
if(resFile.isBinaryXml()){
decodeResXml(outDir, resFile);
}else {
decodeResRaw(outDir, resFile);
}
addDecodedPath(resFile.getFilePath());
}
private void decodeResRaw(File outDir, ResFile resFile)
throws IOException {
Entry entry = resFile.pickOne();
PackageBlock packageBlock= entry.getPackageBlock();
File pkgDir=new File(outDir, getPackageDirName(packageBlock));
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);
}
private void decodeResXml(File outDir, ResFile resFile)
throws IOException{
Entry entry = resFile.pickOne();
PackageBlock packageBlock = entry.getPackageBlock();
File pkgDir = new File(outDir, getPackageDirName(packageBlock));
String alias = resFile.buildPath(ApkUtil.RES_DIR_NAME);
String path = alias.replace('/', File.separatorChar);
path = path.replace('/', File.separatorChar);
File file = new File(pkgDir, path);
logVerbose("Decoding: " + path);
serializeXml(packageBlock.getId(), resFile.getInputSource(), file);
resFile.setFilePath(alias);
addDecodedEntry(entry);
}
private ResXmlDocumentSerializer getDocumentSerializer(){
if(documentSerializer == null){
documentSerializer = new ResXmlDocumentSerializer(apkModule);
documentSerializer.setValidateXmlNamespace(true);
}
return documentSerializer;
}
private void decodePublicXml(TableBlock tableBlock, File outDir)
throws IOException{
for(PackageBlock packageBlock:tableBlock.listPackages()){
decodePublicXml(packageBlock, outDir);
}
if(tableBlock.getPackageArray().childesCount()==0){
decodeEmptyTable(outDir);
}
}
private void decodeEmptyTable(File outDir) throws IOException {
logMessage("Decoding empty table ...");
String pkgName = apkModule.getPackageName();
if(pkgName==null){
return;
}
File pkgDir = new File(outDir, "0-"+pkgName);
File resDir = new File(pkgDir, ApkUtil.RES_DIR_NAME);
File values = new File(resDir, "values");
File pubXml = new File(values, ApkUtil.FILE_NAME_PUBLIC_XML);
XMLDocument xmlDocument = new XMLDocument("resources");
xmlDocument.save(pubXml, false);
}
private void decodePublicXml(PackageBlock packageBlock, File outDir)
throws IOException {
String packageDirName=getPackageDirName(packageBlock);
logMessage("Decoding public.xml: "+packageDirName);
File file=new File(outDir, packageDirName);
file=new File(file, ApkUtil.RES_DIR_NAME);
file=new File(file, "values");
file=new File(file, ApkUtil.FILE_NAME_PUBLIC_XML);
PackageIdentifier packageIdentifier = new PackageIdentifier();
packageIdentifier.load(packageBlock);
packageIdentifier.writePublicXml(file);
}
private void decodeAndroidManifest(File outDir, AndroidManifestBlock manifestBlock)
throws IOException {
if(!apkModule.hasAndroidManifestBlock()){
logMessage("Don't have: "+ AndroidManifestBlock.FILE_NAME);
return;
}
File file=new File(outDir, AndroidManifestBlock.FILE_NAME);
logMessage("Decoding: "+file.getName());
int currentPackageId = manifestBlock.guessCurrentPackageId();
serializeXml(currentPackageId, manifestBlock, file);
addDecodedPath(AndroidManifestBlock.FILE_NAME);
}
private void serializeXml(int currentPackageId, ResXmlDocument document, File outFile)
throws IOException {
XMLNamespaceValidator.validateNamespaces(document);
ResXmlDocumentSerializer serializer = getDocumentSerializer();
if(currentPackageId != 0){
serializer.getDecoder().setCurrentPackageId(currentPackageId);
}
try {
serializer.write(document, outFile);
} catch (XmlPullParserException ex) {
throw new IOException("Error: "+outFile.getName(), ex);
}
}
private void serializeXml(int currentPackageId, InputSource inputSource, File outFile)
throws IOException {
ResXmlDocumentSerializer serializer = getDocumentSerializer();
if(currentPackageId != 0){
serializer.getDecoder().setCurrentPackageId(currentPackageId);
}
try {
serializer.write(inputSource, outFile);
} catch (XmlPullParserException ex) {
throw new IOException("Error: "+outFile.getName(), ex);
}
}
private void addDecodedEntry(Entry entry){
if(entry.isNull()){
return;
}
int resourceId= entry.getResourceId();
Set<ResConfig> resConfigSet=decodedEntries.get(resourceId);
if(resConfigSet==null){
resConfigSet=new HashSet<>();
decodedEntries.put(resourceId, resConfigSet);
}
resConfigSet.add(entry.getResConfig());
}
private boolean containsDecodedEntry(Entry entry){
Set<ResConfig> resConfigSet=decodedEntries.get(entry.getResourceId());
if(resConfigSet==null){
return false;
}
return resConfigSet.contains(entry.getResConfig());
}
private void decodeValues(File outDir, TableBlock tableBlock) throws IOException {
for(PackageBlock packageBlock:tableBlock.listPackages()){
decodeValues(outDir, packageBlock);
}
}
private void decodeValues(File outDir, PackageBlock packageBlock) throws IOException {
logMessage("Decoding values: "
+ getPackageDirName(packageBlock));
packageBlock.sortTypes();
File pkgDir = new File(outDir, getPackageDirName(packageBlock));
File resDir = new File(pkgDir, ApkUtil.RES_DIR_NAME);
for(SpecTypePair specTypePair : packageBlock.listSpecTypePairs()){
decodeValues(resDir, specTypePair);
}
}
private void decodeValues(File outDir, SpecTypePair specTypePair) throws IOException {
entrySerializer.decode(outDir, specTypePair);
}
private String getPackageDirName(PackageBlock packageBlock){
String name = ApkUtil.sanitizeForFileName(packageBlock.getName());
if(name==null){
name="package";
}
TableBlock tableBlock = packageBlock.getTableBlock();
int index = packageBlock.getIndex();
String prefix;
if(index < 10 && tableBlock.countPackages() > 10){
prefix = "0" + index;
}else {
prefix = Integer.toString(index);
}
return prefix + "-" + name;
}
private void extractRootFiles(File outDir) throws IOException {
logMessage("Extracting root files");
File rootDir = new File(outDir, "root");
for(InputSource inputSource:apkModule.getApkArchive().listInputSources()){
if(containsDecodedPath(inputSource.getAlias())){
continue;
}
extractRootFiles(rootDir, inputSource);
addDecodedPath(inputSource.getAlias());
}
}
private void extractRootFiles(File rootDir, InputSource inputSource) throws IOException {
String path=inputSource.getAlias();
path=path.replace(File.separatorChar, '/');
File file=new File(rootDir, path);
File dir=file.getParentFile();
if(!dir.exists()){
dir.mkdirs();
}
FileOutputStream outputStream=new FileOutputStream(file);
inputSource.write(outputStream);
outputStream.close();
}
@Override
public boolean test(Entry entry) {
return !containsDecodedEntry(entry);
}
}

View File

@@ -0,0 +1,97 @@
/*
* 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.APKArchive;
import com.reandroid.archive.FileInputSource;
import com.reandroid.apk.xmlencoder.RESEncoder;
import com.reandroid.archive2.block.ApkSignatureBlock;
import com.reandroid.arsc.chunk.TableBlock;
import com.reandroid.arsc.pool.TableStringPool;
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;
public class ApkModuleXmlEncoder {
private final RESEncoder resEncoder;
public ApkModuleXmlEncoder(){
this.resEncoder = new RESEncoder();
}
public ApkModuleXmlEncoder(ApkModule module, TableBlock tableBlock){
this.resEncoder = new RESEncoder(module, tableBlock);
}
public void scanDirectory(File mainDirectory) throws IOException, XMLException {
loadUncompressedFiles(mainDirectory);
resEncoder.scanDirectory(mainDirectory);
File rootDir=new File(mainDirectory, "root");
scanRootDir(rootDir);
restorePathMap(mainDirectory);
restoreSignatures(mainDirectory);
sortFiles();
TableStringPool tableStringPool = getApkModule().getTableBlock().getTableStringPool();
tableStringPool.removeUnusedStrings();
}
private void restoreSignatures(File dir) throws IOException {
File sigDir = new File(dir, ApkUtil.SIGNATURE_DIR_NAME);
if(!sigDir.isDirectory()){
return;
}
ApkModule apkModule = getApkModule();
apkModule.logMessage("Loading signatures ...");
ApkSignatureBlock signatureBlock = new ApkSignatureBlock();
signatureBlock.scanSplitFiles(sigDir);
apkModule.setApkSignatureBlock(signatureBlock);
}
private void restorePathMap(File dir) throws IOException{
File file = new File(dir, PathMap.JSON_FILE);
if(!file.isFile()){
return;
}
PathMap pathMap = new PathMap();
JSONArray jsonArray = new JSONArray(file);
pathMap.fromJson(jsonArray);
pathMap.restore(getApkModule());
}
public ApkModule getApkModule(){
return resEncoder.getApkModule();
}
private void scanRootDir(File rootDir){
APKArchive archive=getApkModule().getApkArchive();
List<File> rootFileList=ApkUtil.recursiveFiles(rootDir);
for(File file:rootFileList){
String path=ApkUtil.toArchivePath(rootDir, file);
FileInputSource inputSource=new FileInputSource(file, path);
archive.add(inputSource);
}
}
private void sortFiles(){
APKArchive archive = getApkModule().getApkArchive();
archive.autoSortApkFiles();
}
private void loadUncompressedFiles(File mainDirectory) throws IOException {
File file=new File(mainDirectory, UncompressedFiles.JSON_FILE);
UncompressedFiles uncompressedFiles = getApkModule().getUncompressedFiles();
uncompressedFiles.fromJson(file);
}
public void setApkLogger(APKLogger apkLogger) {
this.resEncoder.setAPKLogger(apkLogger);
}
}

View File

@@ -0,0 +1,200 @@
/*
* 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.archive2.block.ApkSignatureBlock;
import java.io.File;
import java.util.*;
public class ApkUtil {
public static String sanitizeForFileName(String name){
if(name==null){
return null;
}
StringBuilder builder = new StringBuilder();
char[] chars = name.toCharArray();
boolean skipNext = true;
int length = 0;
int lengthMax = MAX_FILE_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');
}
public static int parseHex(String hex){
long l=Long.decode(hex);
return (int) l;
}
public static String replaceRootDir(String path, String dirName){
int i=path.indexOf('/')+1;
path=path.substring(i);
if(dirName != null && dirName.length()>0){
if(!dirName.endsWith("/")){
dirName=dirName+"/";
}
path=dirName+path;
}
return path;
}
public static String toArchiveResourcePath(File dir, File file){
String path = toArchivePath(dir, file);
if(path.endsWith(ApkUtil.JSON_FILE_EXTENSION)){
int i2=path.length()- ApkUtil.JSON_FILE_EXTENSION.length();
path=path.substring(0, i2);
}
return path;
}
public static String toArchivePath(File dir, File file){
String dirPath = dir.getAbsolutePath()+File.separator;
String path = file.getAbsolutePath().substring(dirPath.length());
path=path.replace(File.separatorChar, '/');
return path;
}
public static List<File> recursiveFiles(File dir, String ext){
List<File> results=new ArrayList<>();
if(dir.isFile()){
if(hasExtension(dir, ext)){
results.add(dir);
}
return results;
}
if(!dir.isDirectory()){
return results;
}
File[] files=dir.listFiles();
if(files==null){
return results;
}
for(File file:files){
if(file.isFile()){
if(!hasExtension(file, ext)){
continue;
}
results.add(file);
continue;
}
results.addAll(recursiveFiles(file, ext));
}
return results;
}
public static List<File> recursiveFiles(File dir){
return recursiveFiles(dir, null);
}
public static List<File> listDirectories(File dir){
List<File> results=new ArrayList<>();
File[] files=dir.listFiles();
if(files==null){
return results;
}
for(File file:files){
if(file.isDirectory()){
results.add(file);
}
}
return results;
}
public static List<File> listFiles(File dir, String ext){
List<File> results=new ArrayList<>();
File[] files=dir.listFiles();
if(files==null){
return results;
}
for(File file:files){
if(file.isFile()){
if(!hasExtension(file, ext)){
continue;
}
results.add(file);
}
}
return results;
}
private static boolean hasExtension(File file, String ext){
if(ext==null){
return true;
}
String name=file.getName().toLowerCase();
ext=ext.toLowerCase();
return name.endsWith(ext);
}
public static String toModuleName(File file){
String name=file.getName();
int i=name.lastIndexOf('.');
if(i>0){
name=name.substring(0,i);
}
return name;
}
public static Map<String, InputSource> toAliasMap(Collection<InputSource> sourceList){
Map<String, InputSource> results=new HashMap<>();
for(InputSource inputSource:sourceList){
results.put(inputSource.getAlias(), inputSource);
}
return results;
}
public static final String JSON_FILE_EXTENSION=".json";
public static final String RES_JSON_NAME="res-json";
public static final String ROOT_NAME="root";
public static final String SPLIT_JSON_DIRECTORY="resources";
public static final String DEF_MODULE_NAME="base";
public static final String NAME_value_type="value_type";
public static final String NAME_data="data";
public static final String RES_DIR_NAME="res";
public static final String FILE_NAME_PUBLIC_XML ="public.xml";
public static final String TAG_STRING_ARRAY = "string-array";
public static final String TAG_INTEGER_ARRAY = "integer-array";
public static final String SIGNATURE_FILE_NAME = "signatures" + ApkSignatureBlock.FILE_EXT;
public static final String SIGNATURE_DIR_NAME = "signatures";
private static final int MAX_FILE_NAME_LENGTH = 50;
}

View File

@@ -0,0 +1,55 @@
/*
* 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.ByteInputSource;
import com.reandroid.arsc.base.Block;
import com.reandroid.arsc.chunk.Chunk;
import java.io.IOException;
import java.io.OutputStream;
public class BlockInputSource<T extends Chunk<?>> extends ByteInputSource{
private final T mBlock;
public BlockInputSource(String name, T block) {
super(new byte[0], name);
this.mBlock=block;
}
public T getBlock() {
mBlock.refresh();
return mBlock;
}
@Override
public long getLength() throws IOException{
Block block = getBlock();
return block.countBytes();
}
@Override
public long getCrc() throws IOException{
Block block = getBlock();
CrcOutputStream outputStream=new CrcOutputStream();
block.writeBytes(outputStream);
return outputStream.getCrcValue();
}
@Override
public long write(OutputStream outputStream) throws IOException {
return getBlock().writeBytes(outputStream);
}
@Override
public byte[] getBytes() {
return getBlock().getBytes();
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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 java.io.IOException;
import java.io.OutputStream;
import java.util.zip.CRC32;
public class CrcOutputStream extends OutputStream {
private final CRC32 crc;
private long length;
private long mCheckSum;
public CrcOutputStream() {
super();
this.crc = new CRC32();
}
public long getLength(){
return length;
}
public long getCrcValue(){
if(mCheckSum==0){
mCheckSum=crc.getValue();
}
return mCheckSum;
}
@Override
public void write(int b) throws IOException {
this.crc.update(b);
length=length+1;
}
@Override
public void write(byte[] b) throws IOException {
this.write(b, 0, b.length);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
this.crc.update(b, off, len);
length=length+len;
}
}

View File

@@ -0,0 +1,66 @@
/*
* 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.Comparator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class DexFileInputSource extends RenamedInputSource<InputSource> implements Comparable<DexFileInputSource>{
public DexFileInputSource(String name, InputSource inputSource){
super(name, inputSource);
}
public int getDexNumber(){
return getDexNumber(getAlias());
}
@Override
public int compareTo(DexFileInputSource source) {
return Integer.compare(getDexNumber(), source.getDexNumber());
}
public static void sort(List<DexFileInputSource> sourceList){
sourceList.sort(new Comparator<DexFileInputSource>() {
@Override
public int compare(DexFileInputSource s1, DexFileInputSource s2) {
return s1.compareTo(s2);
}
});
}
public static boolean isDexName(String name){
return getDexNumber(name)>=0;
}
static String getDexName(int i){
if(i==0){
return "classes.dex";
}
return "classes"+i+".dex";
}
static int getDexNumber(String name){
Matcher matcher=PATTERN.matcher(name);
if(!matcher.find()){
return -1;
}
String num=matcher.group(1);
if(num.length()==0){
return 0;
}
return Integer.parseInt(num);
}
private static final Pattern PATTERN=Pattern.compile("^classes([0-9]*)\\.dex$");
}

View File

@@ -0,0 +1,96 @@
/*
* 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.io.IOException;
import java.io.InputStream;
public class FileMagic {
public static String getExtensionFromMagic(InputSource inputSource) throws IOException {
byte[] magic=readFileMagic(inputSource);
if(magic==null){
return null;
}
if(isPng(magic)){
return ".png";
}
if(isJpeg(magic)){
return ".jpg";
}
if(isWebp(magic)){
return ".webp";
}
if(isTtf(magic)){
return ".ttf";
}
return null;
}
private static boolean isJpeg(byte[] magic){
return compareMagic(MAGIC_JPG, magic);
}
private static boolean isPng(byte[] magic){
return compareMagic(MAGIC_PNG, magic);
}
private static boolean isWebp(byte[] magic){
return compareMagic(MAGIC_WEBP, magic);
}
private static boolean isTtf(byte[] magic){
return compareMagic(MAGIC_TTF, magic);
}
private static boolean compareMagic(byte[] magic, byte[] readMagic){
if(magic==null || readMagic==null){
return false;
}
int max=magic.length;
if(max>readMagic.length){
max=readMagic.length;
}
if(max==0){
return false;
}
for(int i=0;i<max;i++){
int m=magic[i];
if(m==-1){
continue;
}
if(m != readMagic[i]){
return false;
}
}
return true;
}
private static byte[] readFileMagic(InputSource inputSource) throws IOException {
InputStream inputStream=inputSource.openStream();
byte[] magic=new byte[MAGIC_MAX_LENGTH];
int count=inputStream.read(magic, 0, magic.length);
inputStream.close();
if(count<magic.length){
return null;
}
return magic;
}
private static final int MAGIC_MAX_LENGTH=16;
private static final byte[] MAGIC_PNG=new byte[]{(byte) 137, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a};
private static final byte[] MAGIC_JPG=new byte[]{-0x01, (byte) 0xd8, -0x01, (byte) 224, 0x00, 0x10, 0x4a, 0x46};
private static final byte[] MAGIC_WEBP=new byte[]{0x52, 0x49, 0x46, 0x46, -0x01, -0x01, -0x01, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38};
private static final byte[] MAGIC_TTF=new byte[]{0x00, 0x01, 0x00, 0x00, 0x00, -0x01, -0x01, -0x01};
}

View File

@@ -0,0 +1,279 @@
/*
* 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.APKArchive;
import com.reandroid.archive.ByteInputSource;
import com.reandroid.archive.InputSource;
import com.reandroid.archive.InputSourceUtil;
import com.reandroid.archive2.Archive;
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.util.FrameworkTable;
import com.reandroid.arsc.value.ValueType;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.Objects;
/*
* Produces compressed framework apk by removing irrelevant files and entries,
* basically it keeps only resources.arsc and AndroidManifest.xml
*/
public class FrameworkApk extends ApkModule{
private final Object mLock = new Object();
private int versionCode;
private String versionName;
private String packageName;
private boolean mOptimizing;
private boolean mDestroyed;
public FrameworkApk(String moduleName, APKArchive apkArchive) {
super(moduleName, apkArchive);
super.setLoadDefaultFramework(false);
}
public FrameworkApk(APKArchive apkArchive) {
this("framework", apkArchive);
}
public void destroy(){
synchronized (mLock){
this.versionCode = -1;
this.versionName = "-1";
this.packageName = "destroyed";
super.destroy();
this.mDestroyed = true;
}
}
public boolean isDestroyed() {
synchronized (mLock){
if(!mDestroyed){
return false;
}
if(hasTableBlock()){
this.versionCode = 0;
this.versionName = null;
this.packageName = null;
mDestroyed = false;
return false;
}
return true;
}
}
public int getVersionCode() {
if(this.versionCode == 0){
initValues();
}
return this.versionCode;
}
public String getVersionName() {
if(this.versionName == null){
initValues();
}
return this.versionName;
}
@Override
public String getPackageName() {
if(this.packageName == null){
initValues();
}
return this.packageName;
}
@Override
public void setPackageName(String packageName) {
super.setPackageName(packageName);
this.packageName = null;
}
private void initValues() {
if(hasAndroidManifestBlock()){
AndroidManifestBlock manifest = getAndroidManifestBlock();
Integer code = manifest.getVersionCode();
if(code!=null){
this.versionCode = code;
}
if(this.versionName == null){
this.versionName = manifest.getVersionName();
}
if(this.packageName == null){
this.packageName = manifest.getPackageName();
}
}
if(hasTableBlock()){
FrameworkTable table = getTableBlock();
if(this.versionCode == 0 && table.isOptimized()){
int version = table.getVersionCode();
if(version!=0){
versionCode = version;
if(this.versionName == null){
this.versionName = String.valueOf(version);
}
}
}
if(this.packageName == null){
PackageBlock packageBlock = table.pickOne();
if(packageBlock!=null){
this.packageName = packageBlock.getName();
}
}
}
}
@Override
public void setManifest(AndroidManifestBlock manifestBlock){
synchronized (mLock){
super.setManifest(manifestBlock);
this.versionCode = 0;
this.versionName = null;
this.packageName = null;
}
}
@Override
public void setTableBlock(TableBlock tableBlock){
synchronized (mLock){
super.setTableBlock(tableBlock);
this.versionCode = 0;
this.versionName = null;
this.packageName = null;
}
}
@Override
public FrameworkTable getTableBlock() {
return (FrameworkTable) super.getTableBlock();
}
@Override
FrameworkTable loadTableBlock() throws IOException {
APKArchive archive=getApkArchive();
InputSource inputSource = archive.getInputSource(TableBlock.FILE_NAME);
if(inputSource==null){
throw new IOException("Entry not found: "+TableBlock.FILE_NAME);
}
InputStream inputStream = inputSource.openStream();
FrameworkTable frameworkTable=FrameworkTable.load(inputStream);
frameworkTable.setApkFile(this);
BlockInputSource<FrameworkTable> blockInputSource=new BlockInputSource<>(inputSource.getName(), frameworkTable);
blockInputSource.setMethod(inputSource.getMethod());
blockInputSource.setSort(inputSource.getSort());
archive.add(blockInputSource);
return frameworkTable;
}
public void optimize(){
synchronized (mLock){
if(mOptimizing){
return;
}
if(!hasTableBlock()){
mOptimizing = false;
return;
}
FrameworkTable frameworkTable = getTableBlock();
if(frameworkTable.isOptimized()){
mOptimizing = false;
return;
}
FrameworkOptimizer optimizer = new FrameworkOptimizer(this);
optimizer.optimize();
mOptimizing = false;
}
}
public String getName(){
if(isDestroyed()){
return "destroyed";
}
String pkg = getPackageName();
if(pkg==null){
return "";
}
return pkg + "-" + getVersionCode();
}
@Override
public int hashCode(){
return Objects.hash(getClass(), getName());
}
@Override
public boolean equals(Object obj){
if(obj==this){
return true;
}
if(getClass()!=obj.getClass()){
return false;
}
FrameworkApk other = (FrameworkApk) obj;
return getName().equals(other.getName());
}
@Override
public String toString(){
return getName();
}
public static FrameworkApk loadApkFile(File apkFile) throws IOException {
Archive archive = new Archive(apkFile);
APKArchive apkArchive = new APKArchive(archive.mapEntrySource());
return new FrameworkApk(apkArchive);
}
public static FrameworkApk loadApkFile(File apkFile, String moduleName) throws IOException {
Archive archive = new Archive(apkFile);
APKArchive apkArchive = new APKArchive(archive.mapEntrySource());
return new FrameworkApk(moduleName, apkArchive);
}
public static boolean isFramework(ApkModule apkModule) {
if(!apkModule.hasAndroidManifestBlock()){
return false;
}
return isFramework(apkModule.getAndroidManifestBlock());
}
public static boolean isFramework(AndroidManifestBlock manifestBlock){
ResXmlElement root = manifestBlock.getManifestElement();
ResXmlAttribute attribute = root.getStartElement()
.searchAttributeByName(AndroidManifestBlock.NAME_coreApp);
if(attribute==null || attribute.getValueType()!= ValueType.INT_BOOLEAN){
return false;
}
return attribute.getValueAsBoolean();
}
public static FrameworkApk loadApkBuffer(InputStream inputStream) throws IOException{
return loadApkBuffer("framework", inputStream);
}
public static FrameworkApk loadApkBuffer(String moduleName, InputStream inputStream) throws IOException {
APKArchive archive = new APKArchive();
FrameworkApk frameworkApk = new FrameworkApk(moduleName, archive);
Map<String, ByteInputSource> inputSourceMap = InputSourceUtil.mapInputStreamAsBuffer(inputStream);
ByteInputSource source = inputSourceMap.get(TableBlock.FILE_NAME);
FrameworkTable tableBlock = new FrameworkTable();
if(source!=null){
tableBlock.readBytes(source.openStream());
}
frameworkApk.setTableBlock(tableBlock);
AndroidManifestBlock manifestBlock = new AndroidManifestBlock();
source = inputSourceMap.get(AndroidManifestBlock.FILE_NAME);
if(source!=null){
manifestBlock.readBytes(source.openStream());
}
frameworkApk.setManifest(manifestBlock);
archive.addAll(inputSourceMap.values());
return frameworkApk;
}
public static void optimize(File in, File out, APKLogger apkLogger) throws IOException{
FrameworkApk frameworkApk = FrameworkApk.loadApkFile(in);
frameworkApk.setAPKLogger(apkLogger);
frameworkApk.optimize();
frameworkApk.writeApk(out);
}
}

View File

@@ -0,0 +1,297 @@
/*
* 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.APKArchive;
import com.reandroid.archive.InputSource;
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.chunk.xml.ResXmlNode;
import com.reandroid.arsc.group.EntryGroup;
import com.reandroid.arsc.io.BlockReader;
import com.reandroid.arsc.pool.ResXmlStringPool;
import com.reandroid.arsc.util.FrameworkTable;
import com.reandroid.arsc.value.*;
import java.io.IOException;
import java.util.*;
import java.util.zip.ZipEntry;
public class FrameworkOptimizer {
private final ApkModule frameworkApk;
private APKLogger apkLogger;
private boolean mOptimizing;
public FrameworkOptimizer(ApkModule frameworkApk){
this.frameworkApk = frameworkApk;
this.apkLogger = frameworkApk.getApkLogger();
}
public void optimize(){
if(mOptimizing){
return;
}
mOptimizing = true;
if(!frameworkApk.hasTableBlock()){
logMessage("Don't have: "+TableBlock.FILE_NAME);
mOptimizing = false;
return;
}
FrameworkTable frameworkTable = getFrameworkTable();
AndroidManifestBlock manifestBlock = null;
if(frameworkApk.hasAndroidManifestBlock()){
manifestBlock = frameworkApk.getAndroidManifestBlock();
}
optimizeTable(frameworkTable, manifestBlock);
UncompressedFiles uncompressedFiles = frameworkApk.getUncompressedFiles();
uncompressedFiles.clearExtensions();
uncompressedFiles.clearPaths();
clearFiles(frameworkApk.getApkArchive());
logMessage("Optimized");
}
private void clearFiles(APKArchive archive){
int count = archive.entriesCount();
if(count==2){
return;
}
logMessage("Removing files from: "+count);
InputSource tableSource = archive.getInputSource(TableBlock.FILE_NAME);
InputSource manifestSource = archive.getInputSource(AndroidManifestBlock.FILE_NAME);
archive.clear();
if(tableSource!=null){
tableSource.setMethod(ZipEntry.DEFLATED);
}
if(manifestSource!=null){
manifestSource.setMethod(ZipEntry.DEFLATED);
}
archive.add(tableSource);
archive.add(manifestSource);
count = count - archive.entriesCount();
logMessage("Removed files: "+count);
}
private void optimizeTable(FrameworkTable table, AndroidManifestBlock manifestBlock){
if(table.isOptimized()){
return;
}
logMessage("Optimizing ...");
int prev = table.countBytes();
int version = 0;
String name = "framework";
if(manifestBlock !=null){
Integer code = manifestBlock.getVersionCode();
if(code!=null){
version = code;
}
name = manifestBlock.getPackageName();
compressManifest(manifestBlock);
backupManifestValue(manifestBlock, table);
}
logMessage("Optimizing table ...");
table.optimize(name, version);
long diff=prev - table.countBytes();
long percent=(diff*100L)/prev;
logMessage("Table size reduced by: "+percent+" %");
mOptimizing = false;
}
private FrameworkTable getFrameworkTable(){
TableBlock tableBlock = frameworkApk.getTableBlock();
if(tableBlock instanceof FrameworkTable){
return (FrameworkTable) tableBlock;
}
FrameworkTable frameworkTable = toFramework(tableBlock);
frameworkApk.setTableBlock(frameworkTable);
return frameworkTable;
}
private FrameworkTable toFramework(TableBlock tableBlock){
logMessage("Converting to framework ...");
BlockReader reader = new BlockReader(tableBlock.getBytes());
FrameworkTable frameworkTable = new FrameworkTable();
try {
frameworkTable.readBytes(reader);
} catch (IOException exception) {
logError("Error re-loading framework: ", exception);
}
return frameworkTable;
}
private void compressManifest(AndroidManifestBlock manifestBlock){
logMessage("Compressing manifest ...");
int prev = manifestBlock.countBytes();
ResXmlElement manifest = manifestBlock.getResXmlElement();
List<ResXmlNode> removeList = getManifestElementToRemove(manifest);
for(ResXmlNode node:removeList){
manifest.removeNode(node);
}
ResXmlElement application = manifestBlock.getApplicationElement();
if(application!=null){
removeList = application.listXmlNodes();
for(ResXmlNode node:removeList){
application.removeNode(node);
}
}
ResXmlStringPool stringPool = manifestBlock.getStringPool();
stringPool.removeUnusedStrings();
manifestBlock.refresh();
long diff=prev - manifestBlock.countBytes();
long percent=(diff*100L)/prev;
logMessage("Manifest size reduced by: "+percent+" %");
}
private List<ResXmlNode> getManifestElementToRemove(ResXmlElement manifest){
List<ResXmlNode> results = new ArrayList<>();
for(ResXmlNode node:manifest.listXmlNodes()){
if(!(node instanceof ResXmlElement)){
continue;
}
ResXmlElement element = (ResXmlElement)node;
if(AndroidManifestBlock.TAG_application.equals(element.getTag())){
continue;
}
results.add(element);
}
return results;
}
private void backupManifestValue(AndroidManifestBlock manifestBlock, TableBlock tableBlock){
logMessage("Backup manifest values ...");
ResXmlElement application = manifestBlock.getApplicationElement();
ResXmlAttribute iconAttribute = null;
int iconReference = 0;
if(application!=null){
ResXmlAttribute attribute = application
.searchAttributeByResourceId(AndroidManifestBlock.ID_icon);
if(attribute!=null && attribute.getValueType()==ValueType.REFERENCE){
iconAttribute = attribute;
iconReference = attribute.getData();
}
}
ResXmlElement element = manifestBlock.getResXmlElement();
backupAttributeValues(tableBlock, element);
if(iconAttribute!=null){
iconAttribute.setTypeAndData(ValueType.REFERENCE, iconReference);
}
}
private void backupAttributeValues(TableBlock tableBlock, ResXmlElement element){
if(element==null){
return;
}
for(ResXmlAttribute attribute: element.listAttributes()){
backupAttributeValues(tableBlock, attribute);
}
for(ResXmlElement child: element.listElements()){
backupAttributeValues(tableBlock, child);
}
}
private void backupAttributeValues(TableBlock tableBlock, ResXmlAttribute attribute){
if(attribute==null){
return;
}
ValueType valueType = attribute.getValueType();
if(valueType!=ValueType.REFERENCE && valueType!=ValueType.ATTRIBUTE){
return;
}
int reference = attribute.getData();
Entry entry = getEntryWithValue(tableBlock, reference);
if(entry == null || isReferenceEntry(entry) || entry.isComplex()){
return;
}
ResTableEntry resTableEntry = (ResTableEntry) entry.getTableEntry();
ResValue resValue = resTableEntry.getValue();
valueType = resValue.getValueType();
if(valueType==ValueType.STRING){
String value = resValue.getValueAsString();
attribute.setValueAsString(value);
}else {
int data = resValue.getData();
attribute.setTypeAndData(valueType, data);
}
}
private Entry getEntryWithValue(TableBlock tableBlock, int resourceId){
Set<Integer> circularReference = new HashSet<>();
return getEntryWithValue(tableBlock, resourceId, circularReference);
}
private Entry getEntryWithValue(TableBlock tableBlock, int resourceId, Set<Integer> circularReference){
if(circularReference.contains(resourceId)){
return null;
}
circularReference.add(resourceId);
EntryGroup entryGroup = tableBlock.getEntryGroup(resourceId);
Entry entry = entryGroup.pickOne();
if(entry==null){
return null;
}
if(isReferenceEntry(entry)){
return getEntryWithValue(
tableBlock,
((ResValue)entry.getTableEntry().getValue()).getData(),
circularReference);
}
if(!entry.isNull()){
return entry;
}
Iterator<Entry> itr = entryGroup.iterator(true);
while (itr.hasNext()){
entry = itr.next();
if(!isReferenceEntry(entry)){
if(!entry.isNull()){
return entry;
}
}
}
return null;
}
private boolean isReferenceEntry(Entry entry){
if(entry==null || entry.isNull()){
return false;
}
TableEntry<?, ?> tableEntry = entry.getTableEntry();
if(tableEntry instanceof CompoundEntry){
return false;
}
if(!(tableEntry instanceof ResTableEntry)){
return false;
}
ResTableEntry resTableEntry = (ResTableEntry) tableEntry;
ResValue resValue = resTableEntry.getValue();
ValueType valueType = resValue.getValueType();
return valueType == ValueType.REFERENCE
|| valueType == ValueType.ATTRIBUTE;
}
APKLogger getApkLogger(){
return apkLogger;
}
public void setAPKLogger(APKLogger logger) {
this.apkLogger = logger;
}
void logMessage(String msg) {
if(apkLogger!=null){
apkLogger.logMessage(msg);
}
}
private void logError(String msg, Throwable tr) {
if(apkLogger!=null){
apkLogger.logError(msg, tr);
}
}
private void logVerbose(String msg) {
if(apkLogger!=null){
apkLogger.logVerbose(msg);
}
}
}

View File

@@ -0,0 +1,36 @@
/*
* 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.FileInputSource;
import com.reandroid.archive.InputSource;
import com.reandroid.arsc.chunk.xml.AndroidManifestBlock;
import java.io.File;
public class JsonManifestInputSource extends JsonXmlInputSource {
public JsonManifestInputSource(InputSource inputSource) {
super(inputSource);
}
AndroidManifestBlock newInstance(){
return new AndroidManifestBlock();
}
public static JsonManifestInputSource fromFile(File rootDir, File jsonFile){
String path=ApkUtil.toArchiveResourcePath(rootDir, jsonFile);
FileInputSource fileInputSource=new FileInputSource(jsonFile, path);
return new JsonManifestInputSource(fileInputSource);
}
}

Some files were not shown because too many files have changed in this diff Show More