core: replace resources ids with names from '.arsc' file
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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 + "}";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<String, ClassNode> names = new HashMap<String, ClassNode>();
|
||||
private final ErrorsCounter errorsCounter = new ErrorsCounter();
|
||||
|
||||
private List<DexNode> dexNodes;
|
||||
private Map<Integer, String> resourcesNames = new HashMap<Integer, String>();
|
||||
@Nullable
|
||||
private String appPackage;
|
||||
private ClassNode appResClass;
|
||||
|
||||
public void load(List<InputFile> dexFiles) throws DecodeException {
|
||||
dexNodes = new ArrayList<DexNode>(dexFiles.size());
|
||||
@@ -49,6 +68,58 @@ public class RootNode {
|
||||
initInnerClasses(classes);
|
||||
}
|
||||
|
||||
public void loadResources(List<ResourceFile> 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<ClassNode> 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<Integer, String> getResourcesNames() {
|
||||
return resourcesNames;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getAppPackage() {
|
||||
return appPackage;
|
||||
}
|
||||
|
||||
public ClassNode getAppResClass() {
|
||||
return appResClass;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -19,6 +19,7 @@ public class ResourceStorage {
|
||||
};
|
||||
|
||||
private final List<ResourceEntry> list = new ArrayList<ResourceEntry>();
|
||||
private String appPackage;
|
||||
|
||||
public Collection<ResourceEntry> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Integer, String> 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<Integer, String> resMap) {
|
||||
this.resMap = resMap;
|
||||
}
|
||||
|
||||
protected void noDebugInfo() {
|
||||
this.withDebugInfo = false;
|
||||
}
|
||||
|
||||
@@ -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<Integer, String> map = new HashMap<Integer, String>();
|
||||
map.put(2131230730, "id.Button");
|
||||
setResMap(map);
|
||||
|
||||
ClassNode cls = getClassNode(TestCls.class);
|
||||
String code = cls.getCode().toString();
|
||||
assertThat(code, containsOne("return R.id.Button;"));
|
||||
}
|
||||
}
|
||||
@@ -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<Integer, String> map = new HashMap<Integer, String>();
|
||||
map.put(2131230730, "id.Button");
|
||||
setResMap(map);
|
||||
|
||||
ClassNode cls = getClassNode(TestCls.class);
|
||||
String code = cls.getCode().toString();
|
||||
assertThat(code, containsOne("return R.id.Button;"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user