diff --git a/README.md b/README.md index 2a9287cfe..cc501ef6c 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,10 @@ options: 'ignore' - don't read and don't save --deobf-use-sourcename - use source file name as class name alias --deobf-parse-kotlin-metadata - parse kotlin metadata to class and package names + --deobf-res-name-source - better name source for resources: + 'auto' - automatically select best name (default) + 'resources' - use resources names + 'code' - use R class fields names --use-kotlin-methods-for-var-names - use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide, default: apply --rename-flags - fix options (comma-separated list of): 'case' - fix case sensitivity issues (according to --fs-case-sensitive option), diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java index b80c783bb..884a93455 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java @@ -21,6 +21,7 @@ import jadx.api.JadxArgs.RenameEnum; import jadx.api.JadxArgs.UseKotlinMethodsForVarNames; import jadx.api.JadxDecompiler; import jadx.api.args.DeobfuscationMapFileMode; +import jadx.api.args.ResourceNameSource; import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.files.FileUtils; @@ -129,6 +130,16 @@ public class JadxCLIArgs { @Parameter(names = { "--deobf-parse-kotlin-metadata" }, description = "parse kotlin metadata to class and package names") protected boolean deobfuscationParseKotlinMetadata = false; + @Parameter( + names = { "--deobf-res-name-source" }, + description = "better name source for resources:" + + "\n 'auto' - automatically select best name (default)" + + "\n 'resources' - use resources names" + + "\n 'code' - use R class fields names", + converter = ResourceNameSourceConverter.class + ) + protected ResourceNameSource resourceNameSource = ResourceNameSource.AUTO; + @Parameter( names = { "--use-kotlin-methods-for-var-names" }, description = "use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide", @@ -262,6 +273,7 @@ public class JadxCLIArgs { args.setUseSourceNameAsClassAlias(deobfuscationUseSourceNameAsAlias); args.setParseKotlinMetadata(deobfuscationParseKotlinMetadata); args.setUseKotlinMethodsForVarNames(useKotlinMethodsForVarNames); + args.setResourceNameSource(resourceNameSource); args.setEscapeUnicode(escapeUnicode); args.setRespectBytecodeAccModifiers(respectBytecodeAccessModifiers); args.setExportAsGradleProject(exportAsGradleProject); @@ -378,6 +390,10 @@ public class JadxCLIArgs { return deobfuscationParseKotlinMetadata; } + public ResourceNameSource getResourceNameSource() { + return resourceNameSource; + } + public UseKotlinMethodsForVarNames getUseKotlinMethodsForVarNames() { return useKotlinMethodsForVarNames; } @@ -502,6 +518,19 @@ public class JadxCLIArgs { } } + public static class ResourceNameSourceConverter implements IStringConverter { + @Override + public ResourceNameSource convert(String value) { + try { + return ResourceNameSource.valueOf(value.toUpperCase()); + } catch (Exception e) { + throw new IllegalArgumentException( + '\'' + value + "' is unknown, possible values are: " + + JadxCLIArgs.enumValuesString(ResourceNameSource.values())); + } + } + } + public static class DecompilationModeConverter implements IStringConverter { @Override public DecompilationMode convert(String value) { diff --git a/jadx-core/src/main/java/jadx/api/JadxArgs.java b/jadx-core/src/main/java/jadx/api/JadxArgs.java index 3b3feab37..aa94c978f 100644 --- a/jadx-core/src/main/java/jadx/api/JadxArgs.java +++ b/jadx-core/src/main/java/jadx/api/JadxArgs.java @@ -16,6 +16,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.args.DeobfuscationMapFileMode; +import jadx.api.args.ResourceNameSource; import jadx.api.data.ICodeData; import jadx.api.impl.AnnotatedCodeWriter; import jadx.api.impl.InMemoryCodeCache; @@ -72,6 +73,7 @@ public class JadxArgs { private File deobfuscationMapFile = null; private DeobfuscationMapFileMode deobfuscationMapFileMode = DeobfuscationMapFileMode.READ; + private ResourceNameSource resourceNameSource = ResourceNameSource.AUTO; private int deobfuscationMinLength = 0; private int deobfuscationMaxLength = Integer.MAX_VALUE; @@ -369,6 +371,14 @@ public class JadxArgs { this.deobfuscationMapFile = deobfuscationMapFile; } + public ResourceNameSource getResourceNameSource() { + return resourceNameSource; + } + + public void setResourceNameSource(ResourceNameSource resourceNameSource) { + this.resourceNameSource = resourceNameSource; + } + public boolean isEscapeUnicode() { return escapeUnicode; } @@ -540,6 +550,7 @@ public class JadxArgs { String argStr = "args:" + decompilationMode + useImports + showInconsistentCode + inlineAnonymousClasses + inlineMethods + deobfuscationOn + deobfuscationMinLength + deobfuscationMaxLength + + resourceNameSource + parseKotlinMetadata + useKotlinMethodsForVarNames + insertDebugLines + extractFinally + debugInfo + useSourceNameAsClassAlias + escapeUnicode + replaceConsts @@ -564,6 +575,7 @@ public class JadxArgs { + ", deobfuscationOn=" + deobfuscationOn + ", deobfuscationMapFile=" + deobfuscationMapFile + ", deobfuscationMapFileMode=" + deobfuscationMapFileMode + + ", resourceNameSource=" + resourceNameSource + ", useSourceNameAsClassAlias=" + useSourceNameAsClassAlias + ", parseKotlinMetadata=" + parseKotlinMetadata + ", useKotlinMethodsForVarNames=" + useKotlinMethodsForVarNames diff --git a/jadx-core/src/main/java/jadx/api/args/ResourceNameSource.java b/jadx-core/src/main/java/jadx/api/args/ResourceNameSource.java new file mode 100644 index 000000000..7d9915531 --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/args/ResourceNameSource.java @@ -0,0 +1,22 @@ +package jadx.api.args; + +/** + * Resources original name source (for deobfuscation) + */ +public enum ResourceNameSource { + + /** + * Automatically select best name (default) + */ + AUTO, + + /** + * Force use resources provided names + */ + RESOURCES, + + /** + * Force use resources names from R class + */ + CODE, +} 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 f7fc912de..6786633ce 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 @@ -657,6 +657,10 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN return contains(AType.ANONYMOUS_CLASS); } + public boolean isSynthetic() { + return contains(AFlag.SYNTHETIC); + } + public boolean isInner() { return parentClass != this; } 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 f2a7d2b9e..3bde140bf 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 @@ -65,6 +65,10 @@ public class FieldNode extends NotificationAttrNode implements ICodeNode { return fieldInfo.getAlias(); } + public void rename(String alias) { + fieldInfo.setAlias(alias); + } + public ArgType getType() { return type; } @@ -73,6 +77,10 @@ public class FieldNode extends NotificationAttrNode implements ICodeNode { return parentClass; } + public ClassNode getTopParentClass() { + return parentClass.getTopParentClass(); + } + public List getUseIn() { return useIn; } diff --git a/jadx-core/src/main/java/jadx/core/utils/BetterName.java b/jadx-core/src/main/java/jadx/core/utils/BetterName.java new file mode 100644 index 000000000..48033a305 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/utils/BetterName.java @@ -0,0 +1,62 @@ +package jadx.core.utils; + +import java.util.HashSet; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.core.deobf.NameMapper; +import jadx.core.deobf.TldHelper; + +public class BetterName { + private static final Logger LOG = LoggerFactory.getLogger(BetterName.class); + + private static final boolean DEBUG = true; + + public static String compareAndGet(String first, String second) { + if (Objects.equals(first, second)) { + return first; + } + int firstRating = calcRating(first); + int secondRating = calcRating(second); + boolean firstBetter = firstRating >= secondRating; + if (DEBUG) { + if (firstBetter) { + LOG.info("Better name: '{}' > '{}' ({} > {})", first, second, firstRating, secondRating); + } else { + LOG.info("Better name: '{}' > '{}' ({} > {})", second, first, secondRating, firstRating); + } + } + return firstBetter ? first : second; + } + + public static int calcRating(String str) { + int rating = str.length() * 3; + rating += differentCharsCount(str) * 20; + + if (NameMapper.isAllCharsPrintable(str)) { + rating += 100; + } + if (NameMapper.isValidIdentifier(str)) { + rating += 50; + } + if (TldHelper.contains(str)) { + rating += 20; + } + if (str.contains("_")) { + // rare in obfuscated names + rating += 100; + } + return rating; + } + + private static int differentCharsCount(String str) { + String lower = str.toLowerCase(Locale.ROOT); + Set chars = new HashSet<>(); + StringUtils.visitCodePoints(lower, chars::add); + return chars.size(); + } +} 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 00ef0d1ab..7528a0f86 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java @@ -10,14 +10,18 @@ import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; +import jadx.api.args.ResourceNameSource; import jadx.core.deobf.NameMapper; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.RootNode; +import jadx.core.utils.BetterName; +import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.xmlgen.entry.EntryConfig; import jadx.core.xmlgen.entry.RawNamedValue; import jadx.core.xmlgen.entry.RawValue; @@ -287,35 +291,66 @@ public class ResTableParser extends CommonBinaryParser implements IResParser { if (renamedKey != null) { return renamedKey; } - FieldNode constField = root.getConstValues().getGlobalConstFields().get(resRef); - if (constField != null) { - constField.add(AFlag.DONT_RENAME); - return constField.getName(); - } - // styles might contain dots in name, use VALID_RES_KEY_PATTERN only for resource file name + // styles might contain dots in name, search for alias only for resources names if (typeName.equals("style")) { return origKeyName; - } else if (VALID_RES_KEY_PATTERN.matcher(origKeyName).matches()) { - return origKeyName; + } + FieldNode constField = root.getConstValues().getGlobalConstFields().get(resRef); + String resAlias = getResAlias(resRef, origKeyName, constField); + resStorage.addRename(resRef, resAlias); + if (constField != null) { + constField.rename(resAlias); + constField.add(AFlag.DONT_RENAME); + } + return resAlias; + } + + private String getResAlias(int resRef, String origKeyName, @Nullable FieldNode constField) { + String name; + if (constField == null || constField.getTopParentClass().isSynthetic()) { + name = origKeyName; + } else { + name = getBetterName(root.getArgs().getResourceNameSource(), origKeyName, constField.getName()); + } + Matcher matcher = VALID_RES_KEY_PATTERN.matcher(name); + if (matcher.matches()) { + return name; } // Making sure origKeyName compliant with resource file name rules - Matcher m = VALID_RES_KEY_PATTERN.matcher(origKeyName); + String cleanedResName = cleanName(matcher); + String newResName = String.format("res_0x%08x", resRef); + if (cleanedResName.isEmpty()) { + return newResName; + } + // autogenerate key name, appended with cleaned origKeyName to be human-friendly + return newResName + "_" + cleanedResName.toLowerCase(); + } + + public static String getBetterName(ResourceNameSource nameSource, String resName, String codeName) { + switch (nameSource) { + case AUTO: + return BetterName.compareAndGet(resName, codeName); + case RESOURCES: + return resName; + case CODE: + return codeName; + + default: + throw new JadxRuntimeException("Unexpected ResourceNameSource value: " + nameSource); + } + } + + private String cleanName(Matcher matcher) { StringBuilder sb = new StringBuilder(); boolean first = true; - while (m.find()) { + while (matcher.find()) { if (!first) { sb.append("_"); } - sb.append(m.group()); + sb.append(matcher.group()); first = false; } - // autogenerate key name, appended with cleaned origKeyName to be human-friendly - String newResName = String.format("res_0x%08x", resRef); - String cleanedResName = sb.toString(); - if (!cleanedResName.isEmpty()) { - newResName += "_" + cleanedResName.toLowerCase(); - } - return newResName; + return sb.toString(); } private RawNamedValue parseValueMap() throws IOException { diff --git a/jadx-core/src/test/java/jadx/core/utils/TestBetterName.java b/jadx-core/src/test/java/jadx/core/utils/TestBetterName.java new file mode 100644 index 000000000..acd747106 --- /dev/null +++ b/jadx-core/src/test/java/jadx/core/utils/TestBetterName.java @@ -0,0 +1,22 @@ +package jadx.core.utils; + +import org.junit.jupiter.api.Test; + +import static jadx.core.utils.BetterName.calcRating; +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestBetterName { + + @Test + public void test() { + expectFirst("color_main", "t0"); + expectFirst("done", "oOo0oO0o"); + } + + private void expectFirst(String first, String second) { + String best = BetterName.compareAndGet(first, second); + assertThat(best) + .as(() -> String.format("'%s'=%d, '%s'=%d", first, calcRating(first), second, calcRating(second))) + .isEqualTo(first); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java index 966c4dfe9..8de45268e 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java @@ -30,6 +30,7 @@ import jadx.api.CommentsLevel; import jadx.api.DecompilationMode; import jadx.api.JadxArgs; import jadx.api.args.DeobfuscationMapFileMode; +import jadx.api.args.ResourceNameSource; import jadx.cli.JadxCLIArgs; import jadx.cli.LogHelper; import jadx.gui.ui.MainWindow; @@ -348,6 +349,10 @@ public class JadxSettings extends JadxCLIArgs { this.useKotlinMethodsForVarNames = useKotlinMethodsForVarNames; } + public void setResourceNameSource(ResourceNameSource source) { + this.resourceNameSource = source; + } + public void updateRenameFlag(JadxArgs.RenameEnum flag, boolean enabled) { if (enabled) { renameFlags.add(flag); diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java index 317bf027f..208fa17d2 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java @@ -63,6 +63,7 @@ import jadx.api.DecompilationMode; import jadx.api.JadxArgs; import jadx.api.JadxArgs.UseKotlinMethodsForVarNames; import jadx.api.args.DeobfuscationMapFileMode; +import jadx.api.args.ResourceNameSource; import jadx.api.plugins.JadxPlugin; import jadx.api.plugins.JadxPluginInfo; import jadx.api.plugins.options.JadxPluginOptions; @@ -271,6 +272,13 @@ public class JadxSettingsWindow extends JDialog { needReload(); }); + JComboBox resNamesSource = new JComboBox<>(ResourceNameSource.values()); + resNamesSource.setSelectedItem(settings.getResourceNameSource()); + resNamesSource.addActionListener(e -> { + settings.setResourceNameSource((ResourceNameSource) resNamesSource.getSelectedItem()); + needReload(); + }); + JComboBox deobfMapFileModeCB = new JComboBox<>(DeobfuscationMapFileMode.values()); deobfMapFileModeCB.setSelectedItem(settings.getDeobfuscationMapFileMode()); deobfMapFileModeCB.addActionListener(e -> { @@ -287,6 +295,7 @@ public class JadxSettingsWindow extends JDialog { deobfGroup.addRow(NLS.str("preferences.deobfuscation_max_len"), maxLenSpinner); deobfGroup.addRow(NLS.str("preferences.deobfuscation_source_alias"), deobfSourceAlias); deobfGroup.addRow(NLS.str("preferences.deobfuscation_kotlin_metadata"), deobfKotlinMetadata); + deobfGroup.addRow(NLS.str("preferences.deobfuscation_res_name_source"), resNamesSource); deobfGroup.addRow(NLS.str("preferences.deobfuscation_map_file_mode"), deobfMapFileModeCB); deobfGroup.end(); diff --git a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties index 4a5bf0c2e..e190fbdb7 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -171,6 +171,7 @@ preferences.deobfuscation_min_len=Minimale Namenlänge preferences.deobfuscation_max_len=Maximale Namenlänge preferences.deobfuscation_source_alias=Quelldateiname als Klassennamen-Alias verwenden preferences.deobfuscation_kotlin_metadata=Kotlin-Metadaten nach Klassen- und Paketnamen analysieren +#preferences.deobfuscation_res_name_source=Better resources name source preferences.save=Speichern preferences.cancel=Abbrechen preferences.reset=Zurücksetzen diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index 9808166d5..a34f34fb5 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -171,6 +171,7 @@ preferences.deobfuscation_min_len=Minimum name length preferences.deobfuscation_max_len=Maximum name length preferences.deobfuscation_source_alias=Use source file name as class name alias preferences.deobfuscation_kotlin_metadata=Parse Kotlin metadata for class and package names +preferences.deobfuscation_res_name_source=Better resources name source preferences.save=Save preferences.cancel=Cancel preferences.reset=Reset diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties index 247c045f7..125367376 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -171,6 +171,7 @@ preferences.deobfuscation_min_len=Longitud mínima del nombre preferences.deobfuscation_max_len=Longitud máxima del nombre preferences.deobfuscation_source_alias=Usar el nombre del source como alias para la clase preferences.deobfuscation_kotlin_metadata=Parse Kotlin metadatos para nombres de clase y paquete +#preferences.deobfuscation_res_name_source=Better resources name source preferences.save=Guardar preferences.cancel=Cancelar preferences.reset=Reestablecer diff --git a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties index c578dbf26..4b7a6b27b 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -171,6 +171,7 @@ preferences.deobfuscation_min_len=최소 이름 길이 preferences.deobfuscation_max_len=최대 이름 길이 preferences.deobfuscation_source_alias=소스 파일 이름을 클래스 이름 별칭으로 사용 preferences.deobfuscation_kotlin_metadata=클래스 및 패키지 이름에 대한 Kotlin 메타 데이터 파싱 +#preferences.deobfuscation_res_name_source=Better resources name source preferences.save=저장 preferences.cancel=취소 preferences.reset=재설정 diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties index 00205dc55..0be460d47 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -171,6 +171,7 @@ preferences.deobfuscation_min_len=最小命名长度 preferences.deobfuscation_max_len=最大命名长度 preferences.deobfuscation_source_alias=使用资源名作为类的别名 preferences.deobfuscation_kotlin_metadata=解析Kotlin元数据以获得类名和包名 +#preferences.deobfuscation_res_name_source=Better resources name source preferences.save=保存 preferences.cancel=取消 preferences.reset=重置 diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties index f8b0f2977..887ddad12 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties @@ -171,6 +171,7 @@ preferences.deobfuscation_min_len=最小名稱長度 preferences.deobfuscation_max_len=最大名稱長度 preferences.deobfuscation_source_alias=將原始檔案名稱作為類別別名 preferences.deobfuscation_kotlin_metadata=剖析 Kotlin 中繼資料來取得類別及套件名稱 +#preferences.deobfuscation_res_name_source=Better resources name source preferences.save=儲存 preferences.cancel=取消 preferences.reset=重設