feat: replace Android resource ids with android.R fields (#2119)

This commit is contained in:
Skylot
2024-03-31 20:16:03 +01:00
parent ecdc4e6757
commit 43c082e4da
8 changed files with 115 additions and 29 deletions
@@ -54,6 +54,7 @@ import jadx.core.dex.visitors.debuginfo.DebugInfoApplyVisitor;
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;
@@ -97,6 +98,7 @@ public class Jadx {
List<IDexTreeVisitor> passes = new ArrayList<>();
passes.add(new SignatureProcessor());
passes.add(new OverrideMethodVisitor());
passes.add(new AddAndroidConstants());
passes.add(new CollectConstValues());
// rename and deobfuscation
@@ -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);
});
}
}
@@ -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);
}
}
}
@@ -19,6 +19,7 @@ import jadx.core.dex.info.ConstStorage;
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;
@@ -390,7 +391,7 @@ public class BinaryXMLParser extends CommonBinaryParser {
// 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('/');
@@ -427,7 +428,7 @@ public class BinaryXMLParser extends CommonBinaryParser {
}
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) {
@@ -17,7 +17,7 @@ import jadx.api.ICodeInfo;
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;
@@ -102,7 +102,7 @@ public class ProtoXMLParser extends CommonProtoParser {
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 /
@@ -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;
@@ -9,8 +8,7 @@ import org.jetbrains.annotations.Nullable;
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;
@@ -18,30 +16,12 @@ 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
@@ -128,7 +108,7 @@ public class ValuesParser extends ParserConstants {
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;
}
@@ -143,7 +123,7 @@ public class ValuesParser extends ParserConstants {
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;
}
@@ -178,7 +158,7 @@ public class ValuesParser extends ParserConstants {
if (ri != null) {
return ri.replace('/', '.');
} else {
String androidRi = androidResMap.get(ref);
String androidRi = AndroidResourcesMap.getResName(ref);
if (androidRi != null) {
return "android:" + androidRi.replace('/', '.');
}
@@ -4,13 +4,15 @@ import java.util.Map;
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();
}
}
@@ -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;");
}
}