From 1d1ca7d0c04ee235c772b6c629aa50df009652fc Mon Sep 17 00:00:00 2001 From: Skylot <118523+skylot@users.noreply.github.com> Date: Sun, 18 May 2025 22:27:12 +0100 Subject: [PATCH] fix: process synthetic resource inner classes using common method (#2482) --- .../main/java/jadx/api/JadxDecompiler.java | 10 ++- .../java/jadx/core/dex/nodes/ClassNode.java | 3 + .../java/jadx/core/dex/nodes/RootNode.java | 20 ++++- .../utils/android/AndroidResourcesUtils.java | 20 ++--- .../java/jadx/tests/api/IntegrationTest.java | 78 +++++++++++++++++-- 5 files changed, 104 insertions(+), 27 deletions(-) diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index 97d6d683d..6d77c9ec0 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -124,13 +124,15 @@ public final class JadxDecompiler implements Closeable { loadPlugins(); loadInputFiles(); - root = new RootNode(args); + root = new RootNode(this); root.init(); - root.setDecompilerRef(this); - root.mergePasses(customPasses); + // load classes and resources root.loadClasses(loadedInputs); - root.initClassPath(); root.loadResources(resourcesLoader, getResources()); + root.finishClassLoad(); + root.initClassPath(); + // init passes + root.mergePasses(customPasses); root.runPreDecompileStage(); root.initPasses(); loadFinished(); 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 e0a051512..f337d539c 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 @@ -838,6 +838,9 @@ public class ClassNode extends NotificationAttrNode return clsInfo.getAliasShortName(); } + /** + * Deprecated. Use {@link #getAlias()} + */ @Deprecated public String getShortName() { return clsInfo.getAliasShortName(); 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 eaebaf7a2..1239e2970 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 @@ -100,7 +100,20 @@ public class RootNode { private @Nullable ManifestAttributes manifestAttributes; + public RootNode(JadxDecompiler decompiler) { + this(decompiler, decompiler.getArgs()); + } + + /** + * Deprecated. Prefer {@link #RootNode(JadxDecompiler)} + */ + @Deprecated public RootNode(JadxArgs args) { + this(null, args); + } + + private RootNode(@Nullable JadxDecompiler decompiler, JadxArgs args) { + this.decompiler = decompiler; this.args = args; this.preDecompilePasses = Jadx.getPreDecompilePassesList(); this.processClasses = new ProcessClass(Jadx.getPassesList(args)); @@ -131,6 +144,9 @@ public class RootNode { Utils.checkThreadInterrupt(); }); } + } + + public void finishClassLoad() { if (classes.size() != clsMap.size()) { // class name duplication detected markDuplicatedClasses(classes); @@ -715,10 +731,6 @@ public class RootNode { return args; } - public void setDecompilerRef(JadxDecompiler jadxDecompiler) { - this.decompiler = jadxDecompiler; - } - public @Nullable JadxDecompiler getDecompiler() { return decompiler; } diff --git a/jadx-core/src/main/java/jadx/core/utils/android/AndroidResourcesUtils.java b/jadx-core/src/main/java/jadx/core/utils/android/AndroidResourcesUtils.java index a42698b06..40c603e24 100644 --- a/jadx-core/src/main/java/jadx/core/utils/android/AndroidResourcesUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/android/AndroidResourcesUtils.java @@ -82,7 +82,7 @@ public class AndroidResourcesUtils { public static boolean isResourceClass(ClassNode cls) { ClassNode parentClass = cls.getParentClass(); - return parentClass != null && parentClass.getShortName().equals("R"); + return parentClass != null && parentClass.getAlias().equals("R"); } private static final class ResClsInfo { @@ -109,7 +109,7 @@ public class AndroidResourcesUtils { for (ClassNode innerClass : resCls.getInnerClasses()) { ResClsInfo innerResCls = new ResClsInfo(innerClass); innerClass.getFields().forEach(field -> innerResCls.getFieldsMap().put(field.getName(), field)); - innerClsMap.put(innerClass.getShortName(), innerResCls); + innerClsMap.put(innerClass.getAlias(), innerResCls); } } for (ResourceEntry resource : resStorage.getResources()) { @@ -142,24 +142,18 @@ public class AndroidResourcesUtils { } } - @NotNull private static ResClsInfo getClassForResType(ClassNode resCls, boolean rClsExists, String typeName) { - String clsFullName = resCls.getFullName() + '$' + typeName; - ClassInfo clsInfo = ClassInfo.fromName(resCls.root(), clsFullName); - ClassNode existCls = resCls.root().resolveClass(clsInfo); + RootNode root = resCls.root(); + String typeClsFullName = resCls.getClassInfo().makeRawFullName() + '$' + typeName; + ClassInfo clsInfo = ClassInfo.fromName(root, typeClsFullName); + ClassNode existCls = root.resolveClass(clsInfo); if (existCls != null) { - if (!rClsExists && !existCls.isInner()) { - // convert found res cls to inner for R class - existCls.getClassInfo().convertToInner(resCls); - resCls.addInnerClass(existCls); - } ResClsInfo resClsInfo = new ResClsInfo(existCls); existCls.getFields().forEach(field -> resClsInfo.getFieldsMap().put(field.getName(), field)); return resClsInfo; } - ClassNode newTypeCls = ClassNode.addSyntheticClass(resCls.root(), clsInfo, + ClassNode newTypeCls = ClassNode.addSyntheticClass(root, clsInfo, AccessFlags.PUBLIC | AccessFlags.STATIC | AccessFlags.FINAL); - resCls.addInnerClass(newTypeCls); if (rClsExists) { newTypeCls.addInfoComment("Added by JADX"); } 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 e877f3c20..7f92ef76f 100644 --- a/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java +++ b/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java @@ -3,6 +3,7 @@ package jadx.tests.api; import java.io.Closeable; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; @@ -43,14 +44,19 @@ import jadx.api.JadxInternalAccess; import jadx.api.JavaClass; import jadx.api.JavaMethod; import jadx.api.JavaVariable; +import jadx.api.ResourceFile; +import jadx.api.ResourceType; +import jadx.api.ResourcesLoader; import jadx.api.args.GeneratedRenamesMappingFileMode; import jadx.api.data.IJavaNodeRef; import jadx.api.data.impl.JadxCodeData; import jadx.api.data.impl.JadxCodeRename; import jadx.api.data.impl.JadxNodeRef; +import jadx.api.impl.SimpleCodeInfo; import jadx.api.metadata.ICodeMetadata; import jadx.api.metadata.annotations.InsnCodeOffset; import jadx.api.metadata.annotations.VarNode; +import jadx.api.plugins.CustomResourcesLoader; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; @@ -58,6 +64,10 @@ import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; +import jadx.core.utils.files.FileUtils; +import jadx.core.xmlgen.BinaryXMLStrings; +import jadx.core.xmlgen.IResTableParser; +import jadx.core.xmlgen.ResContainer; import jadx.core.xmlgen.ResourceStorage; import jadx.core.xmlgen.entry.ResourceEntry; import jadx.tests.api.compiler.CompilerOptions; @@ -258,16 +268,15 @@ public abstract class IntegrationTest extends TestUtils { JadxDecompiler d = new JadxDecompiler(args); try { + insertResources(d); d.load(); + return d; } catch (Exception e) { LOG.error("Load failed", e); d.close(); fail(e.getMessage()); return null; } - RootNode root = JadxInternalAccess.getRoot(d); - insertResources(root); - return d; } protected void decompileAndCheck(ClassNode cls) { @@ -346,18 +355,75 @@ public abstract class IntegrationTest extends TestUtils { } } - private void insertResources(RootNode root) { + /** + * Insert mock resource table data ('.arsc' file) + */ + private void insertResources(JadxDecompiler decompiler) throws IOException { if (resMap.isEmpty()) { return; } - ResourceStorage resStorage = new ResourceStorage(root.getArgs().getSecurity()); + String resTableName = "test-res-table"; + Path resTablePath = testDir.resolve(resTableName); + File resTableFile = resTablePath.toFile().getAbsoluteFile(); + FileUtils.makeDirsForFile(resTableFile); + Files.writeString(resTablePath, resTableName); + JadxArgs jadxArgs = decompiler.getArgs(); + jadxArgs.getInputFiles().add(resTableFile); + + // load mock file as 'arsc' + decompiler.addCustomResourcesLoader(new CustomResourcesLoader() { + @Override + public boolean load(ResourcesLoader loader, List list, File file) { + if (file == resTableFile) { + list.add(ResourceFile.createResourceFile(decompiler, resTableFile, ResourceType.ARSC)); + return true; + } + return false; + } + + @Override + public void close() { + } + }); + + // convert resources map to resource storage object + ResourceStorage resStorage = new ResourceStorage(jadxArgs.getSecurity()); for (Map.Entry entry : resMap.entrySet()) { Integer id = entry.getKey(); String name = entry.getValue(); String[] parts = name.split("\\."); resStorage.add(new ResourceEntry(id, "", parts[0], parts[1], "")); } - root.processResources(resStorage); + + // mock res table parser to just return resource storage + IResTableParser resTableParser = new IResTableParser() { + @Override + public void decode(InputStream inputStream) { + } + + @Override + public ResourceStorage getResStorage() { + return resStorage; + } + + @Override + public ResContainer decodeFiles() { + return ResContainer.textResource(resTableName, new SimpleCodeInfo(resTableName)); + } + + @Override + public BinaryXMLStrings getStrings() { + return new BinaryXMLStrings(); + } + }; + + // directly return generated resource storage instead parsing for mock res file + decompiler.getResourcesLoader().addResTableParserProvider(resFile -> { + if (resFile.getOriginalName().equals(resTableFile.getAbsolutePath())) { + return resTableParser; + } + return null; + }); } private void runAutoCheck(ClassNode cls) {