You've already forked android_translation_layer
mirror of
https://gitlab.com/android_translation_layer/android_translation_layer.git
synced 2025-10-27 11:48:10 -07:00
replace arsc_parser with ARSClib
This commit is contained in:
2137
src/ARSCLib/com/android/org/kxml2/io/KXmlParser.java
Normal file
2137
src/ARSCLib/com/android/org/kxml2/io/KXmlParser.java
Normal file
File diff suppressed because it is too large
Load Diff
565
src/ARSCLib/com/android/org/kxml2/io/KXmlSerializer.java
Normal file
565
src/ARSCLib/com/android/org/kxml2/io/KXmlSerializer.java
Normal file
File diff suppressed because it is too large
Load Diff
65
src/ARSCLib/com/android/org/kxml2/io/LibCoreStringPool.java
Normal file
65
src/ARSCLib/com/android/org/kxml2/io/LibCoreStringPool.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
22
src/ARSCLib/com/reandroid/apk/APKLogger.java
Normal file
22
src/ARSCLib/com/reandroid/apk/APKLogger.java
Normal 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);
|
||||
}
|
||||
219
src/ARSCLib/com/reandroid/apk/AndroidFrameworks.java
Normal file
219
src/ARSCLib/com/reandroid/apk/AndroidFrameworks.java
Normal 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";
|
||||
}
|
||||
216
src/ARSCLib/com/reandroid/apk/ApkBundle.java
Normal file
216
src/ARSCLib/com/reandroid/apk/ApkBundle.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
110
src/ARSCLib/com/reandroid/apk/ApkDecoder.java
Normal file
110
src/ARSCLib/com/reandroid/apk/ApkDecoder.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
233
src/ARSCLib/com/reandroid/apk/ApkJsonDecoder.java
Normal file
233
src/ARSCLib/com/reandroid/apk/ApkJsonDecoder.java
Normal 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);
|
||||
}
|
||||
}
|
||||
209
src/ARSCLib/com/reandroid/apk/ApkJsonEncoder.java
Normal file
209
src/ARSCLib/com/reandroid/apk/ApkJsonEncoder.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
838
src/ARSCLib/com/reandroid/apk/ApkModule.java
Normal file
838
src/ARSCLib/com/reandroid/apk/ApkModule.java
Normal file
File diff suppressed because it is too large
Load Diff
320
src/ARSCLib/com/reandroid/apk/ApkModuleXmlDecoder.java
Normal file
320
src/ARSCLib/com/reandroid/apk/ApkModuleXmlDecoder.java
Normal 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);
|
||||
}
|
||||
}
|
||||
97
src/ARSCLib/com/reandroid/apk/ApkModuleXmlEncoder.java
Normal file
97
src/ARSCLib/com/reandroid/apk/ApkModuleXmlEncoder.java
Normal 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);
|
||||
}
|
||||
}
|
||||
200
src/ARSCLib/com/reandroid/apk/ApkUtil.java
Normal file
200
src/ARSCLib/com/reandroid/apk/ApkUtil.java
Normal 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;
|
||||
}
|
||||
55
src/ARSCLib/com/reandroid/apk/BlockInputSource.java
Normal file
55
src/ARSCLib/com/reandroid/apk/BlockInputSource.java
Normal 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();
|
||||
}
|
||||
}
|
||||
53
src/ARSCLib/com/reandroid/apk/CrcOutputStream.java
Normal file
53
src/ARSCLib/com/reandroid/apk/CrcOutputStream.java
Normal 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;
|
||||
}
|
||||
}
|
||||
66
src/ARSCLib/com/reandroid/apk/DexFileInputSource.java
Normal file
66
src/ARSCLib/com/reandroid/apk/DexFileInputSource.java
Normal 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$");
|
||||
|
||||
}
|
||||
96
src/ARSCLib/com/reandroid/apk/FileMagic.java
Normal file
96
src/ARSCLib/com/reandroid/apk/FileMagic.java
Normal 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};
|
||||
|
||||
}
|
||||
279
src/ARSCLib/com/reandroid/apk/FrameworkApk.java
Normal file
279
src/ARSCLib/com/reandroid/apk/FrameworkApk.java
Normal 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);
|
||||
}
|
||||
}
|
||||
297
src/ARSCLib/com/reandroid/apk/FrameworkOptimizer.java
Normal file
297
src/ARSCLib/com/reandroid/apk/FrameworkOptimizer.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/ARSCLib/com/reandroid/apk/JsonManifestInputSource.java
Normal file
36
src/ARSCLib/com/reandroid/apk/JsonManifestInputSource.java
Normal 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
Reference in New Issue
Block a user