diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index 11389de7b..94e6e880f 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -253,6 +253,8 @@ public final class JadxDecompiler { root = new RootNode(); LOG.info("loading ..."); root.load(inputFiles); + root.loadResources(getResources()); + root.initAppResClass(); } void processClass(ClassNode cls) { diff --git a/jadx-core/src/main/java/jadx/api/ResourceFile.java b/jadx-core/src/main/java/jadx/api/ResourceFile.java index 8a05ad83e..cb661fc48 100644 --- a/jadx-core/src/main/java/jadx/api/ResourceFile.java +++ b/jadx-core/src/main/java/jadx/api/ResourceFile.java @@ -49,13 +49,17 @@ public class ResourceFile { } public CodeWriter getContent() { - return ResourcesLoader.loadContent(decompiler, zipRef, type); + return ResourcesLoader.loadContent(decompiler, this); } void setZipRef(ZipRef zipRef) { this.zipRef = zipRef; } + ZipRef getZipRef() { + return zipRef; + } + @Override public String toString() { return "ResourceFile{name='" + name + '\'' + ", type=" + type + "}"; diff --git a/jadx-core/src/main/java/jadx/api/ResourcesLoader.java b/jadx-core/src/main/java/jadx/api/ResourcesLoader.java index b1497ca1c..d65fa2e08 100644 --- a/jadx-core/src/main/java/jadx/api/ResourcesLoader.java +++ b/jadx-core/src/main/java/jadx/api/ResourcesLoader.java @@ -41,7 +41,12 @@ public final class ResourcesLoader { return list; } - static CodeWriter loadContent(JadxDecompiler jadxRef, ZipRef zipRef, ResourceType type) { + public static interface ResourceDecoder { + Object decode(long size, InputStream is) throws IOException; + } + + public static Object decodeStream(ResourceFile rf, ResourceDecoder decoder) throws JadxException { + ZipRef zipRef = rf.getZipRef(); if (zipRef == null) { return null; } @@ -50,18 +55,13 @@ public final class ResourcesLoader { try { zipFile = new ZipFile(zipRef.getZipFile()); ZipEntry entry = zipFile.getEntry(zipRef.getEntryName()); - if (entry != null) { - if (entry.getSize() > LOAD_SIZE_LIMIT) { - return new CodeWriter().add("File too big, size: " - + String.format("%.2f KB", entry.getSize() / 1024.)); - } - inputStream = new BufferedInputStream(zipFile.getInputStream(entry)); - return decode(jadxRef, type, inputStream); - } else { - LOG.warn("Zip entry not found: {}", zipRef); + if (entry == null) { + throw new IOException("Zip entry not found: " + zipRef); } - } catch (IOException e) { - LOG.error("Error load: " + zipRef, e); + inputStream = new BufferedInputStream(zipFile.getInputStream(entry)); + return decoder.decode(entry.getSize(), inputStream); + } catch (Exception e) { + throw new JadxException("Error load: " + zipRef, e); } finally { try { if (zipFile != null) { @@ -74,10 +74,27 @@ public final class ResourcesLoader { LOG.debug("Error close zip file: " + zipRef, e); } } + } + + static CodeWriter loadContent(final JadxDecompiler jadxRef, final ResourceFile rf) { + try { + return (CodeWriter) decodeStream(rf, new ResourceDecoder() { + @Override + public Object decode(long size, InputStream is) throws IOException { + if (size > LOAD_SIZE_LIMIT) { + return new CodeWriter().add("File too big, size: " + + String.format("%.2f KB", size / 1024.)); + } + return loadContent(jadxRef, rf.getType(), is); + } + }); + } catch (JadxException e) { + LOG.error("Decode error", e); + } return null; } - private static CodeWriter decode(JadxDecompiler jadxRef, ResourceType type, + private static CodeWriter loadContent(JadxDecompiler jadxRef, ResourceType type, InputStream inputStream) throws IOException { switch (type) { case MANIFEST: @@ -103,7 +120,7 @@ public final class ResourcesLoader { addEntry(list, file, entry); } } catch (IOException e) { - LOG.debug("Not a zip file: " + file.getAbsolutePath()); + LOG.debug("Not a zip file: {}", file.getAbsolutePath()); } finally { if (zip != null) { try { diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java index 1712a3ee8..ee08d3766 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java @@ -35,6 +35,7 @@ import com.android.dex.ClassData; import com.android.dex.ClassData.Field; import com.android.dex.ClassData.Method; import com.android.dex.ClassDef; +import com.android.dx.rop.code.AccessFlags; public class ClassNode extends LineAttrNode implements ILoadable { private static final Logger LOG = LoggerFactory.getLogger(ClassNode.class); @@ -126,6 +127,17 @@ public class ClassNode extends LineAttrNode implements ILoadable { } } + // empty synthetic class + public ClassNode(DexNode dex, ClassInfo clsInfo) { + this.dex = dex; + this.clsInfo = clsInfo; + this.interfaces = Collections.emptyList(); + this.methods = Collections.emptyList(); + this.fields = Collections.emptyList(); + this.accessFlags = new AccessInfo(AccessFlags.ACC_PUBLIC | AccessFlags.ACC_SYNTHETIC, AFType.CLASS); + this.parentClass = this; + } + private void loadAnnotations(ClassDef cls) { int offset = cls.getAnnotationsOffset(); if (offset != 0) { @@ -266,6 +278,12 @@ public class ClassNode extends LineAttrNode implements ILoadable { if (field == null && searchGlobal) { field = dex.getConstFields().get(obj); } + if (field == null && obj instanceof Integer) { + String str = dex.root().getResourcesNames().get(obj); + if (str != null) { + return new ResRefField(dex, str); + } + } return field; } diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/FieldNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/FieldNode.java index 9be5b6e71..d73e3aee3 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/FieldNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/FieldNode.java @@ -23,6 +23,13 @@ public class FieldNode extends LineAttrNode { this.accFlags = new AccessInfo(field.getAccessFlags(), AFType.FIELD); } + public FieldNode(ClassNode cls, FieldInfo fieldInfo, int accessFlags) { + this.parent = cls; + this.fieldInfo = fieldInfo; + this.type = fieldInfo.getType(); + this.accFlags = new AccessInfo(accessFlags, AFType.FIELD); + } + public FieldInfo getFieldInfo() { return fieldInfo; } diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/ResRefField.java b/jadx-core/src/main/java/jadx/core/dex/nodes/ResRefField.java new file mode 100644 index 000000000..e741a2879 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/ResRefField.java @@ -0,0 +1,15 @@ +package jadx.core.dex.nodes; + +import jadx.core.dex.info.FieldInfo; +import jadx.core.dex.instructions.args.ArgType; + +import com.android.dx.rop.code.AccessFlags; + +public class ResRefField extends FieldNode { + + public ResRefField(DexNode dex, String str) { + super(dex.root().getAppResClass(), + new FieldInfo(dex.root().getAppResClass().getClassInfo(), str, ArgType.INT), + AccessFlags.ACC_PUBLIC); + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java index d7ba7f25c..ce5820a43 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java @@ -1,22 +1,41 @@ package jadx.core.dex.nodes; +import jadx.api.ResourceFile; +import jadx.api.ResourceType; +import jadx.api.ResourcesLoader; import jadx.core.clsp.ClspGraph; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.utils.ErrorsCounter; import jadx.core.utils.exceptions.DecodeException; +import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.files.InputFile; +import jadx.core.xmlgen.ResTableParser; +import jadx.core.xmlgen.ResourceStorage; +import jadx.core.xmlgen.entry.ResourceEntry; import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class RootNode { + private static final Logger LOG = LoggerFactory.getLogger(RootNode.class); + private final Map names = new HashMap(); private final ErrorsCounter errorsCounter = new ErrorsCounter(); + private List dexNodes; + private Map resourcesNames = new HashMap(); + @Nullable + private String appPackage; + private ClassNode appResClass; public void load(List dexFiles) throws DecodeException { dexNodes = new ArrayList(dexFiles.size()); @@ -49,6 +68,58 @@ public class RootNode { initInnerClasses(classes); } + public void loadResources(List resources) { + ResourceFile arsc = null; + for (ResourceFile rf : resources) { + if (rf.getType() == ResourceType.ARSC) { + arsc = rf; + break; + } + } + if (arsc == null) { + LOG.debug("'.arsc' file not found"); + return; + } + final ResTableParser parser = new ResTableParser(); + try { + ResourcesLoader.decodeStream(arsc, new ResourcesLoader.ResourceDecoder() { + @Override + public Object decode(long size, InputStream is) throws IOException { + parser.decode(is); + return null; + } + }); + } catch (JadxException e) { + LOG.error("Failed to parse '.arsc' file", e); + return; + } + + ResourceStorage resStorage = parser.getResStorage(); + appPackage = resStorage.getAppPackage(); + for (ResourceEntry entry : resStorage.getResources()) { + resourcesNames.put(entry.getId(), entry.getTypeName() + "." + entry.getKeyName()); + } + } + + public void initAppResClass() { + ClassNode resCls = null; + if (appPackage != null) { + resCls = searchClassByName(appPackage + ".R"); + } else { + for (ClassNode cls : names.values()) { + if (cls.getShortName().equals("R")) { + resCls = cls; + break; + } + } + } + if (resCls != null) { + appResClass = resCls; + return; + } + appResClass = new ClassNode(dexNodes.get(0), ClassInfo.fromName("R")); + } + private static void initClassPath(List classes) throws IOException, DecodeException { if (!ArgType.isClspSet()) { ClspGraph clsp = new ClspGraph(); @@ -111,4 +182,17 @@ public class RootNode { public ErrorsCounter getErrorsCounter() { return errorsCounter; } + + public Map getResourcesNames() { + return resourcesNames; + } + + @Nullable + public String getAppPackage() { + return appPackage; + } + + public ClassNode getAppResClass() { + return appResClass; + } } diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java index 17da9e263..ce9d69f30 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java @@ -132,6 +132,9 @@ public class ResTableParser extends CommonBinaryParser { } PackageChunk pkg = new PackageChunk(id, name, typeStrings, keyStrings); + if (id == 0x7F) { + resStorage.setAppPackage(name); + } while (is.getPos() < endPos) { long chunkStart = is.getPos(); diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResourceStorage.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResourceStorage.java index 2d6affd84..591d03275 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResourceStorage.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResourceStorage.java @@ -19,6 +19,7 @@ public class ResourceStorage { }; private final List list = new ArrayList(); + private String appPackage; public Collection getResources() { return list; @@ -40,4 +41,12 @@ public class ResourceStorage { } return list.get(index); } + + public String getAppPackage() { + return appPackage; + } + + public void setAppPackage(String appPackage) { + this.appPackage = appPackage; + } } diff --git a/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java b/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java index 4f2274be0..f490ce08c 100644 --- a/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java +++ b/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java @@ -8,6 +8,7 @@ import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.DepthTraversal; import jadx.core.dex.visitors.IDexTreeVisitor; import jadx.core.utils.exceptions.JadxException; @@ -26,8 +27,10 @@ import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.jar.JarOutputStream; import static org.hamcrest.CoreMatchers.containsString; @@ -47,9 +50,10 @@ public abstract class IntegrationTest extends TestUtils { protected boolean outputCFG = false; protected boolean isFallback = false; protected boolean deleteTmpFiles = true; - protected boolean withDebugInfo = true; + protected Map resMap = Collections.emptyMap(); + protected String outDir = "test-out-tmp"; protected boolean compile = true; @@ -72,7 +76,10 @@ public abstract class IntegrationTest extends TestUtils { } catch (JadxException e) { fail(e.getMessage()); } - ClassNode cls = JadxInternalAccess.getRoot(d).searchClassByName(clsName); + RootNode root = JadxInternalAccess.getRoot(d); + root.getResourcesNames().putAll(resMap); + + ClassNode cls = root.searchClassByName(clsName); assertNotNull("Class not found: " + clsName, cls); assertEquals(cls.getFullName(), clsName); @@ -340,6 +347,10 @@ public abstract class IntegrationTest extends TestUtils { return files; } + public void setResMap(Map resMap) { + this.resMap = resMap; + } + protected void noDebugInfo() { this.withDebugInfo = false; } diff --git a/jadx-core/src/test/java/jadx/tests/integration/inner/TestRFieldRestore.java b/jadx-core/src/test/java/jadx/tests/integration/inner/TestRFieldRestore.java new file mode 100644 index 000000000..4f763ddbd --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/inner/TestRFieldRestore.java @@ -0,0 +1,35 @@ +package jadx.tests.integration.inner; + +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.IntegrationTest; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; + +import static jadx.tests.api.utils.JadxMatchers.containsOne; +import static org.junit.Assert.assertThat; + +public class TestRFieldRestore extends IntegrationTest { + + public static class TestCls { + public int test() { + return 2131230730; + } + } + + @Test + public void test() { + // unknown R class + disableCompilation(); + + Map map = new HashMap(); + map.put(2131230730, "id.Button"); + setResMap(map); + + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + assertThat(code, containsOne("return R.id.Button;")); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/inner/TestRFieldRestore2.java b/jadx-core/src/test/java/jadx/tests/integration/inner/TestRFieldRestore2.java new file mode 100644 index 000000000..a3f8d2d9a --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/inner/TestRFieldRestore2.java @@ -0,0 +1,39 @@ +package jadx.tests.integration.inner; + +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.IntegrationTest; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; + +import static jadx.tests.api.utils.JadxMatchers.containsOne; +import static org.junit.Assert.assertThat; + +public class TestRFieldRestore2 extends IntegrationTest { + + public static class TestCls { + + public static class R { + } + + public int test() { + return 2131230730; + } + } + + @Test + public void test() { + // unknown id.Button + disableCompilation(); + + Map map = new HashMap(); + map.put(2131230730, "id.Button"); + setResMap(map); + + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + assertThat(code, containsOne("return R.id.Button;")); + } +}