Skip to content

Commit

Permalink
feat: replace Android resource ids with android.R fields (#2119)
Browse files Browse the repository at this point in the history
  • Loading branch information
skylot committed Mar 31, 2024
1 parent ecdc4e6 commit 43c082e
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 29 deletions.
2 changes: 2 additions & 0 deletions jadx-core/src/main/java/jadx/core/Jadx.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
import jadx.core.dex.visitors.debuginfo.DebugInfoAttachVisitor;
import jadx.core.dex.visitors.finaly.MarkFinallyVisitor;
import jadx.core.dex.visitors.kotlin.ProcessKotlinInternals;
import jadx.core.dex.visitors.prepare.AddAndroidConstants;
import jadx.core.dex.visitors.prepare.CollectConstValues;
import jadx.core.dex.visitors.regions.CheckRegions;
import jadx.core.dex.visitors.regions.CleanRegions;
Expand Down Expand Up @@ -97,6 +98,7 @@ public static List<IDexTreeVisitor> getPreDecompilePassesList() {
List<IDexTreeVisitor> passes = new ArrayList<>();
passes.add(new SignatureProcessor());
passes.add(new OverrideMethodVisitor());
passes.add(new AddAndroidConstants());
passes.add(new CollectConstValues());

// rename and deobfuscation
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package jadx.core.dex.visitors.prepare;

import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.ConstStorage;
import jadx.core.dex.info.FieldInfo;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.AbstractVisitor;
import jadx.core.dex.visitors.JadxVisitor;
import jadx.core.utils.android.AndroidResourcesMap;
import jadx.core.utils.exceptions.JadxException;

// TODO: move this pass to separate "Android plugin"
@JadxVisitor(
name = "AddAndroidConstants",
desc = "Insert Android constants from resource mapping file",
runBefore = {
CollectConstValues.class
}
)
public class AddAndroidConstants extends AbstractVisitor {

private static final String R_CLS = "android.R";
private static final String R_INNER_CLS = R_CLS + '$';

@Override
public void init(RootNode root) throws JadxException {
if (!root.getArgs().isReplaceConsts()) {
return;
}
if (root.resolveClass(R_CLS) != null) {
// Android R class already loaded
return;
}
ConstStorage constStorage = root.getConstValues();
AndroidResourcesMap.getMap().forEach((resId, path) -> {
int sep = path.indexOf('/');
String clsName = R_INNER_CLS + path.substring(0, sep);
String resName = path.substring(sep + 1);
ClassInfo cls = ClassInfo.fromName(root, clsName);
FieldInfo field = FieldInfo.from(root, cls, resName, ArgType.INT);
constStorage.addGlobalConstField(field, resId);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package jadx.core.utils.android;

import java.io.InputStream;
import java.util.Map;

import org.jetbrains.annotations.Nullable;

import jadx.core.utils.exceptions.JadxRuntimeException;

/**
* Store resources id to name mapping
*/
public class AndroidResourcesMap {
private static final Map<Integer, String> RES_MAP = loadBundled();

public static @Nullable String getResName(int resId) {
return RES_MAP.get(resId);
}

public static Map<Integer, String> getMap() {
return RES_MAP;
}

private static Map<Integer, String> loadBundled() {
try (InputStream is = AndroidResourcesMap.class.getResourceAsStream("/android/res-map.txt")) {
return TextResMapFile.read(is);
} catch (Exception e) {
throw new JadxRuntimeException("Failed to load android resource file (res-map.txt)", e);
}
}
}
5 changes: 3 additions & 2 deletions jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.StringUtils;
import jadx.core.utils.android.AndroidResourcesMap;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.xmlgen.entry.ValuesParser;

Expand Down Expand Up @@ -390,7 +391,7 @@ private String getAttributeName(int id) {
// there is no entry uses the values form the XML string pool
if (resourceIds != null && 0 <= id && id < resourceIds.length) {
int resId = resourceIds[id];
String str = ValuesParser.getAndroidResMap().get(resId);
String str = AndroidResourcesMap.getResName(resId);
if (str != null) {
// cut type before /
int typeEnd = str.indexOf('/');
Expand Down Expand Up @@ -427,7 +428,7 @@ private void decodeAttribute(int attributeNS, int attrValDataType, int attrValDa
}
writer.add(resName);
} else {
String androidResName = ValuesParser.getAndroidResMap().get(attrValData);
String androidResName = AndroidResourcesMap.getResName(attrValData);
if (androidResName != null) {
writer.add("@android:").add(androidResName);
} else if (attrValData == 0) {
Expand Down
4 changes: 2 additions & 2 deletions jadx-core/src/main/java/jadx/core/xmlgen/ProtoXMLParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import jadx.api.ICodeWriter;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.StringUtils;
import jadx.core.xmlgen.entry.ValuesParser;
import jadx.core.utils.android.AndroidResourcesMap;

public class ProtoXMLParser extends CommonProtoParser {
private Map<String, String> nsMap;
Expand Down Expand Up @@ -102,7 +102,7 @@ private String getAttributeFullName(XmlAttribute a) {
if (attrName.isEmpty()) {
// some optimization tools clear the name because the Android platform doesn't need it
int resId = a.getResourceId();
String str = ValuesParser.getAndroidResMap().get(resId);
String str = AndroidResourcesMap.getResName(resId);
if (str != null) {
namespace = nsMap.get(ParserConstants.ANDROID_NS_URL);
// cut type before /
Expand Down
28 changes: 4 additions & 24 deletions jadx-core/src/main/java/jadx/core/xmlgen/entry/ValuesParser.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package jadx.core.xmlgen.entry;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
Expand All @@ -9,39 +8,20 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jadx.core.utils.android.TextResMapFile;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.android.AndroidResourcesMap;
import jadx.core.xmlgen.BinaryXMLStrings;
import jadx.core.xmlgen.ParserConstants;
import jadx.core.xmlgen.XmlGenUtils;

public class ValuesParser extends ParserConstants {
private static final Logger LOG = LoggerFactory.getLogger(ValuesParser.class);

private static Map<Integer, String> androidResMap;

private final BinaryXMLStrings strings;
private final Map<Integer, String> resMap;

public ValuesParser(BinaryXMLStrings strings, Map<Integer, String> resMap) {
this.strings = strings;
this.resMap = resMap;
getAndroidResMap();
}

public static Map<Integer, String> getAndroidResMap() {
if (androidResMap == null) {
androidResMap = loadAndroidResMap();
}
return androidResMap;
}

private static Map<Integer, String> loadAndroidResMap() {
try (InputStream is = ValuesParser.class.getResourceAsStream("/android/res-map.txt")) {
return TextResMapFile.read(is);
} catch (Exception e) {
throw new JadxRuntimeException("Failed to load android resource file", e);
}
}

@Nullable
Expand Down Expand Up @@ -128,7 +108,7 @@ public String decodeValue(int dataType, int data) {
case TYPE_REFERENCE: {
String ri = resMap.get(data);
if (ri == null) {
String androidRi = androidResMap.get(data);
String androidRi = AndroidResourcesMap.getResName(data);
if (androidRi != null) {
return "@android:" + androidRi;
}
Expand All @@ -143,7 +123,7 @@ public String decodeValue(int dataType, int data) {
case TYPE_ATTRIBUTE: {
String ri = resMap.get(data);
if (ri == null) {
String androidRi = androidResMap.get(data);
String androidRi = AndroidResourcesMap.getResName(data);
if (androidRi != null) {
return "?android:" + androidRi;
}
Expand Down Expand Up @@ -178,7 +158,7 @@ public String decodeNameRef(int nameRef) {
if (ri != null) {
return ri.replace('/', '.');
} else {
String androidRi = androidResMap.get(ref);
String androidRi = AndroidResourcesMap.getResName(ref);
if (androidRi != null) {
return "android:" + androidRi.replace('/', '.');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@

import org.junit.jupiter.api.Test;

import jadx.core.utils.android.AndroidResourcesMap;

import static org.assertj.core.api.Assertions.assertThat;

class ValuesParserTest {

@Test
void testResMapLoad() {
Map<Integer, String> androidResMap = ValuesParser.getAndroidResMap();
Map<Integer, String> androidResMap = AndroidResourcesMap.getMap();
assertThat(androidResMap).isNotNull().isNotEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package jadx.tests.integration.android;

import org.junit.jupiter.api.Test;

import jadx.tests.api.IntegrationTest;

import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;

public class TestResConstReplace extends IntegrationTest {

public static class TestCls {
public int test() {
return 0x0101013f; // android.R.attr.minWidth
}
}

@Test
public void test() {
disableCompilation();
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("import android.R;")
.containsOne("return R.attr.minWidth;");
}
}

0 comments on commit 43c082e

Please sign in to comment.