This commit is contained in:
REAndroid 2022-12-11 09:42:05 -05:00
parent 661073d9b4
commit b75cb1f163
18 changed files with 298 additions and 46 deletions

View File

@ -2,10 +2,93 @@
## Android binary resources read/write library
```java
import com.reandroid.lib.arsc.chunk.TableBlock;
import com.reandroid.lib.arsc.io.BlockReader;
import com.reandroid.lib.arsc.chunk.TableBlock;
import com.reandroid.lib.arsc.chunk.PackageBlock;
import com.reandroid.lib.arsc.chunk.xml.AndroidManifestBlock;
import com.reandroid.lib.arsc.chunk.xml.ResXmlElement;
import com.reandroid.lib.arsc.chunk.xml.ResXmlAttribute;
public static void example() throws IOException{
public static void exampleManifest() throws IOException{
File inFile=new File("AndroidManifest.xml");
// *** Loading AndroidManifest ***
AndroidManifestBlock manifestBlock = AndroidManifestBlock.load(inFile);
System.out.println("Package name: "+manifestBlock.getPackageName());
List<String> usesPermissionList = manifestBlock.getUsesPermissions();
for(String usesPermission:usesPermissionList){
System.out.println("Uses permission: "+usesPermission);
}
// *** Modifying AndroidManifest ***
// Change package name
manifestBlock.setPackageName("com.new.package-name");
// Add uses-permission
manifestBlock.addUsesPermission("android.permission.WRITE_EXTERNAL_STORAGE");
// Modify version code
manifestBlock.setVersionCode(904);
// Modify version name
manifestBlock.setVersionName("9.0.4");
// Modify xml attribute
List<ResXmlElement> activityList = manifestBlock.listActivities();
for(ResXmlElement activityElement:activityList){
ResXmlAttribute attributeName = activityElement.searchAttributeByResourceId(AndroidManifestBlock.ID_name);
System.out.println("Old activity name: "+attributeName.getValueAsString());
attributeName.setValueAsString("com.app.MyActivity");
System.out.println("New activity name: "+attributeName.getValueAsString());
break;
}
// Refresh to re-calculate offsets
manifestBlock.refresh();
// Save
File outFile=new File("AndroidManifest_out.xml");
manifestBlock.writeBytes(outFile);
System.out.println("Saved: "+outFile);
}
```
```java
public static void exampleResourceTable() throws IOException{
File inFile=new File("resources.arsc");
// *** Loading resource table ***
TableBlock tableBlock=TableBlock.load(inFile);
Collection<PackageBlock> packageBlockList=tableBlock.listPackages();
System.out.println("Packages count = "+packageBlockList.size());
for(PackageBlock packageBlock:packageBlockList){
System.out.println("Package id = "+packageBlock.getId()
+", name = "+packageBlock.getName());
}
// *** Modify resource table
// Change package name
for(PackageBlock packageBlock:packageBlockList){
String name = packageBlock.getName();
String newName = name + ".new-name";
packageBlock.setName(newName);
}
// Refresh to re-calculate offsets
tableBlock.refresh();
// Save
File outFile=new File("resources_out.arsc");
tableBlock.writeBytes(outFile);
System.out.println("Saved: "+outFile);
}
```
```java
public static void exampleLoadApk() throws IOException{
File inFile=new File("test.apk");
File outDir=new File("test_out");
@ -15,7 +98,7 @@
outDir=decoder.writeToDirectory(outDir);
System.out.println("Decoded to: "+outDir);
// You can do any logical modification on any json files
// You can do any logical modification on any json files here
// To convert back json to apk
@ -26,6 +109,8 @@
encodedModule.writeApk(outApk);
System.out.println("Created apk: "+outApk);
}
}
```
[Check implementation on APKEditor](https://github.com/REAndroid/APKEditor)

View File

@ -2,7 +2,7 @@
apply plugin: 'java-library'
group 'com.reandroid.lib.arsc'
version '1.0.3'
version '1.0.4'
java {
sourceCompatibility JavaVersion.VERSION_1_8

View File

@ -0,0 +1,7 @@
package com.reandroid.lib.apk;
public interface APKLogger {
void logMessage(String msg);
void logError(String msg, Throwable tr);
void logVerbose(String msg);
}

View File

@ -14,6 +14,7 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.zip.ZipEntry;
public class ApkModule {
private final String moduleName;
@ -22,6 +23,7 @@ public class ApkModule {
private TableBlock mTableBlock;
private AndroidManifestBlock mManifestBlock;
private final UncompressedFiles mUncompressedFiles;
private APKLogger apkLogger;
ApkModule(String moduleName, APKArchive apkArchive){
this.moduleName=moduleName;
this.apkArchive=apkArchive;
@ -72,6 +74,34 @@ public class ApkModule {
public void removeDir(String dirName){
getApkArchive().removeDir(dirName);
}
public void validateResourcesDir() throws IOException {
List<ResFile> resFileList = listResFiles();
Set<String> existPaths=new HashSet<>();
List<InputSource> sourceList = getApkArchive().listInputSources();
for(InputSource inputSource:sourceList){
existPaths.add(inputSource.getAlias());
}
for(ResFile resFile:resFileList){
String path=resFile.getFilePath();
String pathNew=resFile.validateTypeDirectoryName();
if(pathNew==null || pathNew.equals(path)){
continue;
}
if(existPaths.contains(pathNew)){
continue;
}
existPaths.remove(path);
existPaths.add(pathNew);
resFile.setFilePath(pathNew);
if(resFile.getInputSource().getMethod() == ZipEntry.STORED){
getUncompressedFiles().replacePath(path, pathNew);
}
logVerbose("Dir validated: '"+path+"' -> '"+pathNew+"'");
}
TableStringPool stringPool= getTableBlock().getTableStringPool();
stringPool.refreshUniqueIdMap();
getTableBlock().refresh();
}
public void setResourcesRootDir(String dirName) throws IOException {
List<ResFile> resFileList = listResFiles();
Set<String> existPaths=new HashSet<>();
@ -88,9 +118,14 @@ public class ApkModule {
existPaths.remove(path);
existPaths.add(pathNew);
resFile.setFilePath(pathNew);
if(resFile.getInputSource().getMethod() == ZipEntry.STORED){
getUncompressedFiles().replacePath(path, pathNew);
}
logVerbose("Root changed: '"+path+"' -> '"+pathNew+"'");
}
TableStringPool stringPool= getTableBlock().getTableStringPool();
stringPool.refreshUniqueIdMap();
getTableBlock().refresh();
}
public List<ResFile> listResFiles() throws IOException {
List<ResFile> results=new ArrayList<>();
@ -123,7 +158,7 @@ public class ApkModule {
if(pkg==null){
return null;
}
return pkg.getPackageName();
return pkg.getName();
}
public void setPackageName(String name) throws IOException {
String old=getPackageName();
@ -137,13 +172,13 @@ public class ApkModule {
PackageArray pkgArray = tableBlock.getPackageArray();
for(PackageBlock pkg:pkgArray.listItems()){
if(pkgArray.childesCount()==1){
pkg.setPackageName(name);
pkg.setName(name);
continue;
}
String pkgName=pkg.getPackageName();
String pkgName=pkg.getName();
if(pkgName.startsWith(old)){
pkgName=pkgName.replace(old, name);
pkg.setPackageName(pkgName);
pkg.setName(pkgName);
}
}
}
@ -211,6 +246,24 @@ public class ApkModule {
this.loadDefaultFramework = loadDefaultFramework;
}
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);
}
}
public static ApkModule loadApkFile(File apkFile) throws IOException {
return loadApkFile(apkFile, ApkUtil.DEF_MODULE_NAME);
}

View File

@ -1,6 +1,7 @@
package com.reandroid.lib.apk;
import com.reandroid.archive.InputSource;
import com.reandroid.lib.arsc.chunk.TypeBlock;
import com.reandroid.lib.arsc.chunk.xml.ResXmlBlock;
import com.reandroid.lib.arsc.value.EntryBlock;
import com.reandroid.lib.json.JSONObject;
@ -18,6 +19,51 @@ public class ResFile {
this.inputSource=inputSource;
this.entryBlockList=entryBlockList;
}
public String validateTypeDirectoryName(){
EntryBlock entryBlock=pickOne();
if(entryBlock==null){
return null;
}
String path=getFilePath();
String root="";
int i=path.indexOf('/');
if(i>0){
i++;
root=path.substring(0, i);
path=path.substring(i);
}
String name=path;
i=path.lastIndexOf('/');
if(i>0){
i++;
name=path.substring(i);
}
TypeBlock typeBlock=entryBlock.getTypeBlock();
String typeName=typeBlock.getTypeName()+typeBlock.getResConfig().getQualifiers();
return root+typeName+"/"+name;
}
private EntryBlock pickOne(){
List<EntryBlock> entryList = entryBlockList;
if(entryList.size()==0){
return null;
}
for(EntryBlock entryBlock:entryList){
if(!entryBlock.isNull() && entryBlock.isDefault()){
return entryBlock;
}
}
for(EntryBlock entryBlock:entryList){
if(!entryBlock.isNull()){
return entryBlock;
}
}
for(EntryBlock entryBlock:entryList){
if(entryBlock.isDefault()){
return entryBlock;
}
}
return entryList.get(0);
}
public String getFilePath(){
return getInputSource().getAlias();
}

View File

@ -243,7 +243,7 @@ public class ResourceIds {
return;
}
if(pkg.id!=this.id){
throw new IllegalArgumentException("Different package id: "+this.id+"!="+pkg.id);
throw new DuplicateException("Different package id: "+this.id+"!="+pkg.id);
}
if(pkg.name!=null){
this.name = pkg.name;
@ -277,7 +277,7 @@ public class ResourceIds {
return;
}
if(entry.getPackageId()!=this.id){
throw new IllegalArgumentException("Different package id: "+entry);
throw new DuplicateException("Different package id: "+entry);
}
byte typeId=entry.getTypeId();
Type type=typeMap.get(typeId);
@ -400,7 +400,7 @@ public class ResourceIds {
return;
}
if(this.id!= type.id){
throw new IllegalArgumentException("Different type ids: "+id+"!="+type.id);
throw new DuplicateException("Different type ids: "+id+"!="+type.id);
}
if(type.name!=null){
this.name=type.name;
@ -427,7 +427,7 @@ public class ResourceIds {
return;
}
if(entry.getTypeId()!=this.id){
throw new IllegalArgumentException("Different type id: "+entry);
throw new DuplicateException("Different type id: "+entry);
}
short key=entry.getEntryId();
Entry exist=entryMap.get(key);
@ -435,7 +435,7 @@ public class ResourceIds {
if(Objects.equals(exist.name, entry.name)){
return;
}
throw new IllegalArgumentException("Duplicate entry exist: "+exist+", entry: "+entry);
throw new DuplicateException("Duplicate entry exist: "+exist+", entry: "+entry);
}
if(name == null){
this.name = entry.typeName;
@ -652,10 +652,10 @@ public class ResourceIds {
matcher= pattern.matcher(element);
}
if(id==0){
throw new IllegalArgumentException("Missing id: "+xmlElement);
throw new DuplicateException("Missing id: "+xmlElement);
}
if(name==null){
throw new IllegalArgumentException("Missing name: "+xmlElement);
throw new DuplicateException("Missing name: "+xmlElement);
}
return new Entry(id, type, name);
}
@ -684,6 +684,17 @@ public class ResourceIds {
| (entryId & 0xffff);
}
}
public static class DuplicateException extends IllegalArgumentException{
public DuplicateException(String message){
super(message);
}
public DuplicateException(String message, final Throwable cause) {
super(message, cause);
}
public DuplicateException(Throwable cause) {
super(cause.getMessage(), cause);
}
}
public static final String JSON_FILE_NAME ="resource-ids.json";
}

View File

@ -27,7 +27,7 @@ public class TableBlockJson {
File infoFile=new File(pkgDir, PackageBlock.JSON_FILE_NAME);
JSONObject jsonObject=new JSONObject();
jsonObject.put(PackageBlock.NAME_package_id, packageBlock.getId());
jsonObject.put(PackageBlock.NAME_package_name, packageBlock.getPackageName());
jsonObject.put(PackageBlock.NAME_package_name, packageBlock.getName());
jsonObject.write(infoFile);
for(SpecTypePair specTypePair: packageBlock.listAllSpecTypePair()){
for(TypeBlock typeBlock:specTypePair.getTypeBlockArray().listItems()){
@ -52,7 +52,7 @@ public class TableBlockJson {
private String getDirName(PackageBlock packageBlock){
StringBuilder builder=new StringBuilder();
builder.append(String.format("0x%02x", packageBlock.getId()));
String name= packageBlock.getPackageName();
String name= packageBlock.getName();
if(name!=null){
builder.append('-');
builder.append(name);

View File

@ -70,6 +70,10 @@ public class UncompressedFiles implements JSONConvert<JSONObject> {
return mExtensionList.contains(extension.substring(1));
}
public boolean containsPath(String path){
path=sanitizePath(path);
if(path==null){
return false;
}
return mPathList.contains(path);
}
public void addPath(ZipArchive zipArchive){
@ -84,15 +88,31 @@ public class UncompressedFiles implements JSONConvert<JSONObject> {
addPath(inputSource.getAlias());
}
public void addPath(String path){
if(path==null || path.length()==0){
path=sanitizePath(path);
if(path==null){
return;
}
path=path.replace(File.separatorChar, '/').trim();
while (path.startsWith("/")){
path=path.substring(1);
}
mPathList.add(path);
}
public void removePath(String path){
path=sanitizePath(path);
if(path==null){
return;
}
mPathList.remove(path);
}
public void replacePath(String path, String rep){
path=sanitizePath(path);
rep=sanitizePath(rep);
if(path==null||rep==null){
return;
}
if(!mPathList.contains(path)){
return;
}
mPathList.remove(path);
mPathList.add(rep);
}
public void addCommonExtensions(){
for(String ext:COMMON_EXTENSIONS){
addExtension(ext);
@ -146,6 +166,19 @@ public class UncompressedFiles implements JSONConvert<JSONObject> {
JSONObject jsonObject=new JSONObject(new FileInputStream(jsonFile));
fromJson(jsonObject);
}
private static String sanitizePath(String path){
if(path==null || path.length()==0){
return null;
}
path=path.replace(File.separatorChar, '/').trim();
while (path.startsWith("/")){
path=path.substring(1);
}
if(path.length()==0){
return null;
}
return path;
}
public static final String JSON_FILE = "uncompressed-files.json";
public static final String NAME_paths = "paths";
public static final String NAME_extensions = "extensions";

View File

@ -113,8 +113,6 @@ public class PackageBlock extends BaseChunk implements BlockLoad, JSONConvert<JS
public void setName(String name){
mPackageName.set(name);
}
public TableBlock getTableBlock(){
Block parent=getParent();
while(parent!=null){
@ -125,13 +123,6 @@ public class PackageBlock extends BaseChunk implements BlockLoad, JSONConvert<JS
}
return null;
}
public String getPackageName(){
return mPackageName.get();
}
public void setPackageName(String name){
mPackageName.set(name);
}
public TypeStringPool getTypeStringPool(){
return mTypeStringPool;
}

View File

@ -143,6 +143,9 @@ public class TableBlock extends BaseChunk implements JSONConvert<JSONObject> {
tableBlock.addFramework(Frameworks.getAndroid());
return tableBlock;
}
public static TableBlock load(File file) throws IOException{
return load(new FileInputStream(file));
}
public static TableBlock load(InputStream inputStream) throws IOException{
TableBlock tableBlock=new TableBlock();
tableBlock.readBytes(inputStream);

View File

@ -248,6 +248,16 @@ public class AndroidManifestBlock extends ResXmlBlock{
builder.append("}");
return builder.toString();
}
public static boolean isAndroidManifestBlock(ResXmlBlock xmlBlock){
if(xmlBlock==null){
return false;
}
ResXmlElement root = xmlBlock.getResXmlElement();
if(root==null){
return false;
}
return TAG_manifest.equals(root.getTag());
}
public static AndroidManifestBlock load(File file) throws IOException {
return load(new FileInputStream(file));
}

View File

@ -23,7 +23,7 @@ public class ResXmlAttribute extends FixedBlockContainer
super(7);
mNamespaceReference =new IntegerItem(-1);
mNameReference =new IntegerItem();
mValueStringReference =new IntegerItem();
mValueStringReference =new IntegerItem(-1);
mNameType=new ShortItem((short) 0x0008);
mReserved =new ByteItem();
mValueTypeByte=new ByteItem();
@ -42,7 +42,7 @@ public class ResXmlAttribute extends FixedBlockContainer
int getNamespaceReference(){
return mNamespaceReference.get();
}
void setNamespaceReference(int ref){
public void setNamespaceReference(int ref){
mNamespaceReference.set(ref);
}
int getNameReference(){
@ -112,7 +112,7 @@ public class ResXmlAttribute extends FixedBlockContainer
}
return startNamespace.getPrefix();
}
ResXmlStartNamespace getStartNamespace(){
public ResXmlStartNamespace getStartNamespace(){
ResXmlElement xmlElement=getParentResXmlElement();
if(xmlElement==null){
return null;
@ -131,6 +131,7 @@ public class ResXmlAttribute extends FixedBlockContainer
setValueStringReference(ref);
if(getValueType()==ValueType.STRING){
setRawValue(ref);
setValueStringReference(ref);
}
return resXmlString;
}
@ -280,7 +281,7 @@ public class ResXmlAttribute extends FixedBlockContainer
}
public void setValueAsBoolean(boolean val){
setValueType(ValueType.INT_BOOLEAN);
int ref=val?0xffff:0;
int ref=val?0xffffffff:0;
setRawValue(ref);
setValueStringReference(-1);
}

View File

@ -149,7 +149,7 @@ public class ResXmlElement extends FixedBlockContainer implements JSONConvert<JS
}
return null;
}
public Collection<ResXmlAttribute> listResXmlAttributes(){
public Collection<ResXmlAttribute> listAttributes(){
ResXmlStartElement startElement=getStartElement();
if(startElement!=null){
return startElement.listResXmlAttributes();

View File

@ -31,6 +31,14 @@ public class ResXmlStartElement extends BaseXmlChunk {
addChild(mClassAttribute);
addChild(mAttributeArray);
}
public ResXmlAttribute getAttribute(int resourceId){
for(ResXmlAttribute attribute:listResXmlAttributes()){
if(resourceId==attribute.getNameResourceID()){
return attribute;
}
}
return null;
}
public ResXmlAttribute getAttribute(String uri, String name){
if(name==null){
return null;

View File

@ -25,8 +25,8 @@ public class ValueDecoder {
}
String prefix=null;
if(currentPackage!=null){
String name=currentPackage.getPackageName();
String other=entryBlock.getPackageBlock().getPackageName();
String name=currentPackage.getName();
String other=entryBlock.getPackageBlock().getName();
if(!name.equals(other)){
prefix=other+":";
}
@ -67,7 +67,7 @@ public class ValueDecoder {
is_attribute=true;
}
if(is_reference || is_attribute){
String ref=buildReferenceValue(store, valueType, currentPackage.getPackageName(), data);
String ref=buildReferenceValue(store, valueType, currentPackage.getName(), data);
if(ref!=null){
return ref;
}
@ -286,7 +286,7 @@ public class ValueDecoder {
return null;
}
for(PackageBlock packageBlock:allPkg){
String name=packageBlock.getPackageName();
String name=packageBlock.getName();
if(name!=null){
return name;
}
@ -307,7 +307,7 @@ public class ValueDecoder {
if(packageBlock==null){
return null;
}
return packageBlock.getPackageName();
return packageBlock.getName();
}
private static EntryGroup searchEntryGroup(EntryStore store, EntryBlock entryBlock, int resourceId){
EntryGroup entryGroup=searchEntryGroup(entryBlock, resourceId);

View File

@ -3,6 +3,7 @@ package com.reandroid.lib.arsc.io;
import com.reandroid.lib.arsc.header.HeaderBlock;
import java.io.*;
import java.util.zip.ZipInputStream;
public class BlockReader extends InputStream {
@ -317,6 +318,9 @@ public class BlockReader extends InputStream {
while((len=in.read(buff))>0){
result=add(result, buff, len);
}
if(!(in instanceof ZipInputStream)){
in.close();
}
return result;
}
private static byte[] add(byte[] arr1, byte[] arr2, int len){
@ -331,7 +335,7 @@ public class BlockReader extends InputStream {
}
public static HeaderBlock readHeaderBlock(InputStream inputStream) throws IOException{
byte[] buffer=new byte[8];
inputStream.read(buffer);
inputStream.read(buffer, 0, 8);
inputStream.close();
BlockReader reader=new BlockReader(buffer);
return reader.readHeaderBlock();

View File

@ -273,7 +273,7 @@ public class FrameworkTable extends TableBlock {
for(PackageBlock packageBlock:allPkg){
builder.append("\n ");
builder.append(String.format("0x%02x", packageBlock.getId()));
builder.append(":").append(packageBlock.getPackageName());
builder.append(":").append(packageBlock.getName());
}
return builder.toString();
}

View File

@ -333,7 +333,7 @@ public class EntryBlock extends Block implements JSONConvert<JSONObject> {
if(packageBlock==null){
return null;
}
return packageBlock.getPackageName();
return packageBlock.getName();
}
public SpecString getSpecString(){
PackageBlock packageBlock=getPackageBlock();