From 0b225238fb7e17695c75c61b1fc4ed10b39cd0e8 Mon Sep 17 00:00:00 2001 From: Skylot <118523+skylot@users.noreply.github.com> Date: Fri, 27 Sep 2024 21:07:07 +0100 Subject: [PATCH] feat: support restore of switch over string (basic case)(#2288) --- README.md | 180 +++---- .../main/java/jadx/cli/JCommanderWrapper.java | 6 +- .../src/main/java/jadx/cli/JadxCLIArgs.java | 8 + .../src/main/java/jadx/api/JadxArgs.java | 13 +- jadx-core/src/main/java/jadx/core/Jadx.java | 4 + .../java/jadx/core/codegen/RegionGen.java | 2 + .../java/jadx/core/dex/attributes/AType.java | 2 + .../attributes/nodes/CodeFeaturesAttr.java | 51 ++ .../core/dex/instructions/InsnDecoder.java | 13 +- .../core/dex/instructions/args/InsnArg.java | 10 + .../jadx/core/dex/regions/SwitchRegion.java | 6 + .../regions/SwitchOverStringVisitor.java | 441 ++++++++++++++++++ .../java/jadx/core/utils/InsnRemover.java | 5 +- .../java/jadx/core/utils/RegionUtils.java | 26 ++ .../switches/TestSwitchOverStrings.java | 52 +++ .../java/jadx/gui/settings/JadxSettings.java | 4 + .../gui/settings/ui/JadxSettingsWindow.java | 8 + .../resources/i18n/Messages_de_DE.properties | 1 + .../resources/i18n/Messages_en_US.properties | 1 + .../resources/i18n/Messages_es_ES.properties | 1 + .../resources/i18n/Messages_id_ID.properties | 1 + .../resources/i18n/Messages_ko_KR.properties | 1 + .../resources/i18n/Messages_pt_BR.properties | 1 + .../resources/i18n/Messages_ru_RU.properties | 1 + .../resources/i18n/Messages_zh_CN.properties | 1 + .../resources/i18n/Messages_zh_TW.properties | 1 + 26 files changed, 739 insertions(+), 101 deletions(-) create mode 100644 jadx-core/src/main/java/jadx/core/dex/attributes/nodes/CodeFeaturesAttr.java create mode 100644 jadx-core/src/main/java/jadx/core/dex/visitors/regions/SwitchOverStringVisitor.java create mode 100644 jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchOverStrings.java diff --git a/README.md b/README.md index c827dcc7..582c87be 100644 --- a/README.md +++ b/README.md @@ -91,95 +91,101 @@ commands (use ' --help' for command options): plugins - manage jadx plugins options: - -d, --output-dir - output directory - -ds, --output-dir-src - output directory for sources - -dr, --output-dir-res - output directory for resources - -r, --no-res - do not decode resources - -s, --no-src - do not decompile source code - --single-class - decompile a single class, full name, raw or alias - --single-class-output - file or dir for write if decompile a single class - --output-format - can be 'java' or 'json', default: java - -e, --export-gradle - save as android gradle project - -j, --threads-count - processing threads count, default: 4 - -m, --decompilation-mode - code output mode: - 'auto' - trying best options (default) - 'restructure' - restore code structure (normal java code) - 'simple' - simplified instructions (linear, with goto's) - 'fallback' - raw instructions without modifications - --show-bad-code - show inconsistent code (incorrectly decompiled) - --no-xml-pretty-print - do not prettify XML - --no-imports - disable use of imports, always write entire package name - --no-debug-info - disable debug info parsing and processing - --add-debug-lines - add comments with debug line numbers if available - --no-inline-anonymous - disable anonymous classes inline - --no-inline-methods - disable methods inline - --no-move-inner-classes - disable move inner classes into parent - --no-inline-kotlin-lambda - disable inline for Kotlin lambdas - --no-finally - don't extract finally block - --no-replace-consts - don't replace constant value with matching constant field - --escape-unicode - escape non latin characters in strings (with \u) - --respect-bytecode-access-modifiers - don't change original access modifiers - --mappings-path - deobfuscation mappings file or directory. Allowed formats: Tiny and Tiny v2 (both '.tiny'), Enigma (.mapping) or Enigma directory - --mappings-mode - set mode for handling the deobfuscation mapping file: - 'read' - just read, user can always save manually (default) - 'read-and-autosave-every-change' - read and autosave after every change - 'read-and-autosave-before-closing' - read and autosave before exiting the app or closing the project - 'ignore' - don't read or save (can be used to skip loading mapping files referenced in the project file) - --deobf - activate deobfuscation - --deobf-min - min length of name, renamed if shorter, default: 3 - --deobf-max - max length of name, renamed if longer, default: 64 - --deobf-whitelist - space separated list of classes (full name) and packages (ends with '.*') to exclude from deobfuscation, default: android.support.v4.* android.support.v7.* android.support.v4.os.* android.support.annotation.Px androidx.core.os.* androidx.annotation.Px - --deobf-cfg-file - deobfuscation mappings file used for JADX auto-generated names (in the JOBF file format), default: same dir and name as input file with '.jobf' extension - --deobf-cfg-file-mode - set mode for handling the JADX auto-generated names' deobfuscation map file: - 'read' - read if found, don't save (default) - 'read-or-save' - read if found, save otherwise (don't overwrite) - 'overwrite' - don't read, always save - 'ignore' - don't read and don't save - --deobf-use-sourcename - use source file name as class name alias - --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), - 'valid' - rename java identifiers to make them valid, - 'printable' - remove non-printable chars from identifiers, - or single 'none' - to disable all renames - or single 'all' - to enable all (default) - --integer-format - how integers are displayed: - 'auto' - automatically select (default) - 'decimal' - use decimal - 'hexadecimal' - use hexadecimal - --fs-case-sensitive - treat filesystem as case sensitive, false by default - --cfg - save methods control flow graph to dot file - --raw-cfg - save methods control flow graph (use raw instructions) - -f, --fallback - set '--decompilation-mode' to 'fallback' (deprecated) - --use-dx - use dx/d8 to convert java bytecode - --comments-level - set code comments level, values: error, warn, info, debug, user-only, none, default: info - --log-level - set log level, values: quiet, progress, error, warn, info, debug, default: progress - -v, --verbose - verbose output (set --log-level to DEBUG) - -q, --quiet - turn off output (set --log-level to QUIET) - --version - print jadx version - -h, --help - print this help + -d, --output-dir - output directory + -ds, --output-dir-src - output directory for sources + -dr, --output-dir-res - output directory for resources + -r, --no-res - do not decode resources + -s, --no-src - do not decompile source code + --single-class - decompile a single class, full name, raw or alias + --single-class-output - file or dir for write if decompile a single class + --output-format - can be 'java' or 'json', default: java + -e, --export-gradle - save as android gradle project + -j, --threads-count - processing threads count, default: 4 + -m, --decompilation-mode - code output mode: + 'auto' - trying best options (default) + 'restructure' - restore code structure (normal java code) + 'simple' - simplified instructions (linear, with goto's) + 'fallback' - raw instructions without modifications + --show-bad-code - show inconsistent code (incorrectly decompiled) + --no-xml-pretty-print - do not prettify XML + --no-imports - disable use of imports, always write entire package name + --no-debug-info - disable debug info parsing and processing + --add-debug-lines - add comments with debug line numbers if available + --no-inline-anonymous - disable anonymous classes inline + --no-inline-methods - disable methods inline + --no-move-inner-classes - disable move inner classes into parent + --no-inline-kotlin-lambda - disable inline for Kotlin lambdas + --no-finally - don't extract finally block + --no-restore-switch-over-string - don't restore switch over string + --no-replace-consts - don't replace constant value with matching constant field + --escape-unicode - escape non latin characters in strings (with \u) + --respect-bytecode-access-modifiers - don't change original access modifiers + --mappings-path - deobfuscation mappings file or directory. Allowed formats: Tiny and Tiny v2 (both '.tiny'), Enigma (.mapping) or Enigma directory + --mappings-mode - set mode for handling the deobfuscation mapping file: + 'read' - just read, user can always save manually (default) + 'read-and-autosave-every-change' - read and autosave after every change + 'read-and-autosave-before-closing' - read and autosave before exiting the app or closing the project + 'ignore' - don't read or save (can be used to skip loading mapping files referenced in the project file) + --deobf - activate deobfuscation + --deobf-min - min length of name, renamed if shorter, default: 3 + --deobf-max - max length of name, renamed if longer, default: 64 + --deobf-whitelist - space separated list of classes (full name) and packages (ends with '.*') to exclude from deobfuscation, default: android.support.v4.* android.support.v7.* android.support.v4.os.* android.support.annotation.Px androidx.core.os.* androidx.annotation.Px + --deobf-cfg-file - deobfuscation mappings file used for JADX auto-generated names (in the JOBF file format), default: same dir and name as input file with '.jobf' extension + --deobf-cfg-file-mode - set mode for handling the JADX auto-generated names' deobfuscation map file: + 'read' - read if found, don't save (default) + 'read-or-save' - read if found, save otherwise (don't overwrite) + 'overwrite' - don't read, always save + 'ignore' - don't read and don't save + --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-source-name-as-class-name-alias - use source name as class name alias: + 'always' - always use source name if it's available + 'if-better' - use source name if it seems better than the current one + 'never' - never use source name, even if it's available + --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), + 'valid' - rename java identifiers to make them valid, + 'printable' - remove non-printable chars from identifiers, + or single 'none' - to disable all renames + or single 'all' - to enable all (default) + --integer-format - how integers are displayed: + 'auto' - automatically select (default) + 'decimal' - use decimal + 'hexadecimal' - use hexadecimal + --fs-case-sensitive - treat filesystem as case sensitive, false by default + --cfg - save methods control flow graph to dot file + --raw-cfg - save methods control flow graph (use raw instructions) + -f, --fallback - set '--decompilation-mode' to 'fallback' (deprecated) + --use-dx - use dx/d8 to convert java bytecode + --comments-level - set code comments level, values: error, warn, info, debug, user-only, none, default: info + --log-level - set log level, values: quiet, progress, error, warn, info, debug, default: progress + -v, --verbose - verbose output (set --log-level to DEBUG) + -q, --quiet - turn off output (set --log-level to QUIET) + --version - print jadx version + -h, --help - print this help Plugin options (-P=): - 1) dex-input: Load .dex and .apk files - - dex-input.verify-checksum - verify dex file checksum before load, values: [yes, no], default: yes - 2) java-convert: Convert .class, .jar and .aar files to dex - - java-convert.mode - convert mode, values: [dx, d8, both], default: both - - java-convert.d8-desugar - use desugar in d8, values: [yes, no], default: no - 3) kotlin-metadata: Use kotlin.Metadata annotation for code generation - - kotlin-metadata.class-alias - rename class alias, values: [yes, no], default: yes - - kotlin-metadata.method-args - rename function arguments, values: [yes, no], default: yes - - kotlin-metadata.fields - rename fields, values: [yes, no], default: yes - - kotlin-metadata.companion - rename companion object, values: [yes, no], default: yes - - kotlin-metadata.data-class - add data class modifier, values: [yes, no], default: yes - - kotlin-metadata.to-string - rename fields using toString, values: [yes, no], default: yes - - kotlin-metadata.getters - rename simple getters to field names, values: [yes, no], default: yes - 4) rename-mappings: various mappings support - - rename-mappings.format - mapping format, values: [AUTO, TINY_FILE, TINY_2_FILE, ENIGMA_FILE, ENIGMA_DIR, SRG_FILE, XSRG_FILE, CSRG_FILE, TSRG_FILE, TSRG_2_FILE, PROGUARD_FILE], default: AUTO - - rename-mappings.invert - invert mapping on load, values: [yes, no], default: no + dex-input: Load .dex and .apk files + - dex-input.verify-checksum - verify dex file checksum before load, values: [yes, no], default: yes + java-convert: Convert .class, .jar and .aar files to dex + - java-convert.mode - convert mode, values: [dx, d8, both], default: both + - java-convert.d8-desugar - use desugar in d8, values: [yes, no], default: no + kotlin-metadata: Use kotlin.Metadata annotation for code generation + - kotlin-metadata.class-alias - rename class alias, values: [yes, no], default: yes + - kotlin-metadata.method-args - rename function arguments, values: [yes, no], default: yes + - kotlin-metadata.fields - rename fields, values: [yes, no], default: yes + - kotlin-metadata.companion - rename companion object, values: [yes, no], default: yes + - kotlin-metadata.data-class - add data class modifier, values: [yes, no], default: yes + - kotlin-metadata.to-string - rename fields using toString, values: [yes, no], default: yes + - kotlin-metadata.getters - rename simple getters to field names, values: [yes, no], default: yes + rename-mappings: various mappings support + - rename-mappings.format - mapping format, values: [AUTO, TINY_FILE, TINY_2_FILE, ENIGMA_FILE, ENIGMA_DIR, SRG_FILE, XSRG_FILE, JAM_FILE, CSRG_FILE, TSRG_FILE, TSRG_2_FILE, PROGUARD_FILE, RECAF_SIMPLE_FILE, JOBF_FILE], default: AUTO + - rename-mappings.invert - invert mapping on load, values: [yes, no], default: no + smali-input: Load .smali files + - smali-input.api-level - Android API level, default: 27 Environment variables: JADX_DISABLE_XML_SECURITY - set to 'true' to disable all security checks for XML files diff --git a/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java b/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java index 19028364..6912572b 100644 --- a/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java +++ b/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java @@ -228,7 +228,7 @@ public class JCommanderWrapper { for (PluginContext context : pluginManager.getAllPluginContexts()) { JadxPluginOptions options = context.getOptions(); if (options != null) { - if (appendPlugin(context.getPluginInfo(), context.getOptions(), sb, maxNamesLen, k)) { + if (appendPlugin(context.getPluginInfo(), context.getOptions(), sb, maxNamesLen)) { k++; } } @@ -240,12 +240,12 @@ public class JCommanderWrapper { return "\nPlugin options (-P=):" + sb; } - private boolean appendPlugin(JadxPluginInfo pluginInfo, JadxPluginOptions options, StringBuilder out, int maxNamesLen, int k) { + private boolean appendPlugin(JadxPluginInfo pluginInfo, JadxPluginOptions options, StringBuilder out, int maxNamesLen) { List descs = options.getOptionsDescriptions(); if (descs.isEmpty()) { return false; } - out.append("\n ").append(k).append(") "); + out.append("\n "); out.append(pluginInfo.getPluginId()).append(": ").append(pluginInfo.getDescription()); for (OptionDescription desc : descs) { StringBuilder opt = new StringBuilder(); diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java index a9f9f0b3..184de4da 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java @@ -109,6 +109,9 @@ public class JadxCLIArgs { @Parameter(names = "--no-finally", description = "don't extract finally block") protected boolean extractFinally = true; + @Parameter(names = "--no-restore-switch-over-string", description = "don't restore switch over string") + protected boolean restoreSwitchOverString = true; + @Parameter(names = "--no-replace-consts", description = "don't replace constant value with matching constant field") protected boolean replaceConsts = true; @@ -360,6 +363,7 @@ public class JadxCLIArgs { args.setMoveInnerClasses(moveInnerClasses); args.setAllowInlineKotlinLambda(allowInlineKotlinLambda); args.setExtractFinally(extractFinally); + args.setRestoreSwitchOverString(restoreSwitchOverString); args.setRenameFlags(renameFlags); args.setFsCaseSensitive(fsCaseSensitive); args.setCommentsLevel(commentsLevel); @@ -453,6 +457,10 @@ public class JadxCLIArgs { return extractFinally; } + public boolean isRestoreSwitchOverString() { + return restoreSwitchOverString; + } + public Path getUserRenamesMappingsPath() { return userRenamesMappingsPath; } diff --git a/jadx-core/src/main/java/jadx/api/JadxArgs.java b/jadx-core/src/main/java/jadx/api/JadxArgs.java index eeaca136..143f6d16 100644 --- a/jadx-core/src/main/java/jadx/api/JadxArgs.java +++ b/jadx-core/src/main/java/jadx/api/JadxArgs.java @@ -128,6 +128,8 @@ public class JadxArgs implements Closeable { private boolean respectBytecodeAccModifiers = false; private boolean exportAsGradleProject = false; + private boolean restoreSwitchOverString = true; + private boolean skipXmlPrettyPrint = false; private boolean fsCaseSensitive; @@ -544,6 +546,14 @@ public class JadxArgs implements Closeable { this.exportAsGradleProject = exportAsGradleProject; } + public boolean isRestoreSwitchOverString() { + return restoreSwitchOverString; + } + + public void setRestoreSwitchOverString(boolean restoreSwitchOverString) { + this.restoreSwitchOverString = restoreSwitchOverString; + } + public boolean isSkipXmlPrettyPrint() { return skipXmlPrettyPrint; } @@ -751,7 +761,7 @@ public class JadxArgs implements Closeable { + resourceNameSource + useKotlinMethodsForVarNames + insertDebugLines + extractFinally - + debugInfo + escapeUnicode + replaceConsts + + debugInfo + escapeUnicode + replaceConsts + restoreSwitchOverString + respectBytecodeAccModifiers + fsCaseSensitive + renameFlags + commentsLevel + useDxInput + integerFormat + "|" + buildPluginsHash(decompiler); @@ -796,6 +806,7 @@ public class JadxArgs implements Closeable { + ", deobfuscationWhitelist=" + deobfuscationWhitelist + ", escapeUnicode=" + escapeUnicode + ", replaceConsts=" + replaceConsts + + ", restoreSwitchOverString=" + restoreSwitchOverString + ", respectBytecodeAccModifiers=" + respectBytecodeAccModifiers + ", exportAsGradleProject=" + exportAsGradleProject + ", skipXmlPrettyPrint=" + skipXmlPrettyPrint diff --git a/jadx-core/src/main/java/jadx/core/Jadx.java b/jadx-core/src/main/java/jadx/core/Jadx.java index 3e197507..03f555f5 100644 --- a/jadx-core/src/main/java/jadx/core/Jadx.java +++ b/jadx-core/src/main/java/jadx/core/Jadx.java @@ -62,6 +62,7 @@ import jadx.core.dex.visitors.regions.IfRegionVisitor; import jadx.core.dex.visitors.regions.LoopRegionVisitor; import jadx.core.dex.visitors.regions.RegionMakerVisitor; import jadx.core.dex.visitors.regions.ReturnVisitor; +import jadx.core.dex.visitors.regions.SwitchOverStringVisitor; import jadx.core.dex.visitors.regions.variables.ProcessVariables; import jadx.core.dex.visitors.rename.CodeRenameVisitor; import jadx.core.dex.visitors.rename.RenameVisitor; @@ -170,6 +171,9 @@ public class Jadx { // regions IR passes.add(new RegionMakerVisitor()); passes.add(new IfRegionVisitor()); + if (args.isRestoreSwitchOverString()) { + passes.add(new SwitchOverStringVisitor()); + } passes.add(new ReturnVisitor()); passes.add(new CleanRegions()); diff --git a/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java b/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java index 666d54a3..7942b252 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java @@ -278,6 +278,8 @@ public class RegionGen extends InsnGen { useField(code, (FieldInfo) k, null); } else if (k instanceof Integer) { code.add(TypeGen.literalToString((Integer) k, arg.getType(), mth, fallback)); + } else if (k instanceof String) { + code.add('\"').add((String) k).add('\"'); } else { throw new JadxRuntimeException("Unexpected key in switch: " + (k != null ? k.getClass() : null)); } diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/AType.java b/jadx-core/src/main/java/jadx/core/dex/attributes/AType.java index e70dc572..0cb405db 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/AType.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/AType.java @@ -5,6 +5,7 @@ import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.core.codegen.utils.CodeComment; import jadx.core.dex.attributes.nodes.AnonymousClassAttr; import jadx.core.dex.attributes.nodes.ClassTypeVarsAttr; +import jadx.core.dex.attributes.nodes.CodeFeaturesAttr; import jadx.core.dex.attributes.nodes.DeclareVariablesAttr; import jadx.core.dex.attributes.nodes.EdgeInsnAttr; import jadx.core.dex.attributes.nodes.EnumClassAttr; @@ -74,6 +75,7 @@ public final class AType implements IJadxAttrType { public static final AType METHOD_OVERRIDE = new AType<>(); public static final AType METHOD_TYPE_VARS = new AType<>(); public static final AType> TRY_BLOCKS_LIST = new AType<>(); + public static final AType METHOD_CODE_FEATURES = new AType<>(); // region public static final AType DECLARE_VARIABLES = new AType<>(); diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/CodeFeaturesAttr.java b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/CodeFeaturesAttr.java new file mode 100644 index 00000000..b0fc2b98 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/CodeFeaturesAttr.java @@ -0,0 +1,51 @@ +package jadx.core.dex.attributes.nodes; + +import java.util.EnumSet; +import java.util.Set; + +import jadx.api.plugins.input.data.attributes.IJadxAttribute; +import jadx.core.dex.attributes.AType; +import jadx.core.dex.nodes.MethodNode; + +public class CodeFeaturesAttr implements IJadxAttribute { + + public enum CodeFeature { + /** + * Code contains switch instruction + */ + SWITCH, + } + + public static boolean contains(MethodNode mth, CodeFeature feature) { + CodeFeaturesAttr codeFeaturesAttr = mth.get(AType.METHOD_CODE_FEATURES); + if (codeFeaturesAttr == null) { + return false; + } + return codeFeaturesAttr.getCodeFeatures().contains(feature); + } + + public static void add(MethodNode mth, CodeFeature feature) { + CodeFeaturesAttr codeFeaturesAttr = mth.get(AType.METHOD_CODE_FEATURES); + if (codeFeaturesAttr == null) { + codeFeaturesAttr = new CodeFeaturesAttr(); + mth.addAttr(codeFeaturesAttr); + } + codeFeaturesAttr.getCodeFeatures().add(feature); + } + + private final Set codeFeatures = EnumSet.noneOf(CodeFeature.class); + + public Set getCodeFeatures() { + return codeFeatures; + } + + @Override + public AType getAttrType() { + return AType.METHOD_CODE_FEATURES; + } + + @Override + public String toAttrString() { + return "CodeFeatures{" + codeFeatures + '}'; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/InsnDecoder.java b/jadx-core/src/main/java/jadx/core/dex/instructions/InsnDecoder.java index 06431be6..76956585 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/InsnDecoder.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/InsnDecoder.java @@ -3,10 +3,6 @@ package jadx.core.dex.instructions; import java.util.List; import java.util.Objects; -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import jadx.api.plugins.input.data.ICodeReader; import jadx.api.plugins.input.data.IMethodProto; import jadx.api.plugins.input.data.IMethodRef; @@ -17,6 +13,8 @@ import jadx.api.plugins.input.insns.custom.ISwitchPayload; import jadx.core.Consts; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; +import jadx.core.dex.attributes.nodes.CodeFeaturesAttr; +import jadx.core.dex.attributes.nodes.CodeFeaturesAttr.CodeFeature; import jadx.core.dex.attributes.nodes.JadxError; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.info.MethodInfo; @@ -35,8 +33,6 @@ import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.input.InsnDataUtils; public class InsnDecoder { - private static final Logger LOG = LoggerFactory.getLogger(InsnDecoder.class); - private final MethodNode method; private final RootNode root; @@ -54,7 +50,7 @@ public class InsnDecoder { rawInsn.decode(); insn = decode(rawInsn); } catch (Exception e) { - method.addError("Failed to decode insn: " + rawInsn + ", method: " + method, e); + method.addError("Failed to decode insn: " + rawInsn, e); insn = new InsnNode(InsnType.NOP, 0); insn.addAttr(AType.JADX_ERROR, new JadxError("decode failed: " + e.getMessage(), e)); } @@ -64,7 +60,6 @@ public class InsnDecoder { return instructions; } - @NotNull protected InsnNode decode(InsnData insn) throws DecodeException { switch (insn.getOpcode()) { case NOP: @@ -514,7 +509,6 @@ public class InsnDecoder { } } - @NotNull private SwitchInsn makeSwitch(InsnData insn, boolean packed) { SwitchInsn swInsn = new SwitchInsn(InsnArg.reg(insn, 0, ArgType.UNKNOWN), insn.getTarget(), packed); ICustomPayload payload = insn.getPayload(); @@ -522,6 +516,7 @@ public class InsnDecoder { swInsn.attachSwitchData(new SwitchData((ISwitchPayload) payload), insn.getTarget()); } method.add(AFlag.COMPUTE_POST_DOM); + CodeFeaturesAttr.add(method, CodeFeature.SWITCH); return swInsn; } diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/args/InsnArg.java b/jadx-core/src/main/java/jadx/core/dex/instructions/args/InsnArg.java index 2b78f0d2..1ce323d7 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/args/InsnArg.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/args/InsnArg.java @@ -291,6 +291,16 @@ public abstract class InsnArg extends Typed { return false; } + public boolean isSameCodeVar(RegisterArg arg) { + if (arg == null) { + return false; + } + if (isRegister()) { + return ((RegisterArg) this).sameCodeVar(arg); + } + return false; + } + protected final T copyCommonParams(T copy) { copy.copyAttributesFrom(this); copy.setParentInsn(parentInsn); diff --git a/jadx-core/src/main/java/jadx/core/dex/regions/SwitchRegion.java b/jadx-core/src/main/java/jadx/core/dex/regions/SwitchRegion.java index 045457df..fa4ed9c9 100644 --- a/jadx-core/src/main/java/jadx/core/dex/regions/SwitchRegion.java +++ b/jadx-core/src/main/java/jadx/core/dex/regions/SwitchRegion.java @@ -58,6 +58,12 @@ public final class SwitchRegion extends AbstractRegion implements IBranchRegion cases.add(new CaseInfo(keysList, c)); } + public void addDefaultCase(IContainer c) { + if (c != null) { + cases.add(new CaseInfo(Collections.singletonList(DEFAULT_CASE_KEY), c)); + } + } + public List getCases() { return cases; } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/SwitchOverStringVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/SwitchOverStringVisitor.java new file mode 100644 index 00000000..2813a6a9 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/SwitchOverStringVisitor.java @@ -0,0 +1,441 @@ +package jadx.core.dex.visitors.regions; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.jetbrains.annotations.Nullable; + +import jadx.core.dex.attributes.AFlag; +import jadx.core.dex.attributes.IAttributeNode; +import jadx.core.dex.attributes.nodes.CodeFeaturesAttr; +import jadx.core.dex.attributes.nodes.CodeFeaturesAttr.CodeFeature; +import jadx.core.dex.info.MethodInfo; +import jadx.core.dex.instructions.IfNode; +import jadx.core.dex.instructions.IfOp; +import jadx.core.dex.instructions.InsnType; +import jadx.core.dex.instructions.InvokeNode; +import jadx.core.dex.instructions.args.InsnArg; +import jadx.core.dex.instructions.args.InsnWrapArg; +import jadx.core.dex.instructions.args.LiteralArg; +import jadx.core.dex.instructions.args.RegisterArg; +import jadx.core.dex.instructions.args.SSAVar; +import jadx.core.dex.nodes.IContainer; +import jadx.core.dex.nodes.IRegion; +import jadx.core.dex.nodes.InsnNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.regions.SwitchRegion; +import jadx.core.dex.regions.conditions.IfCondition; +import jadx.core.dex.regions.conditions.IfRegion; +import jadx.core.dex.visitors.AbstractVisitor; +import jadx.core.dex.visitors.JadxVisitor; +import jadx.core.utils.BlockUtils; +import jadx.core.utils.InsnRemover; +import jadx.core.utils.InsnUtils; +import jadx.core.utils.RegionUtils; +import jadx.core.utils.exceptions.JadxException; + +@JadxVisitor( + name = "SwitchOverStringVisitor", + desc = "Restore switch over string", + runAfter = IfRegionVisitor.class, + runBefore = ReturnVisitor.class +) +public class SwitchOverStringVisitor extends AbstractVisitor implements IRegionIterativeVisitor { + + @Override + public void visit(MethodNode mth) throws JadxException { + if (!CodeFeaturesAttr.contains(mth, CodeFeature.SWITCH)) { + return; + } + DepthRegionTraversal.traverseIterative(mth, this); + } + + @Override + public boolean visitRegion(MethodNode mth, IRegion region) { + if (region instanceof SwitchRegion) { + return restoreSwitchOverString(mth, (SwitchRegion) region); + } + return false; + } + + private boolean restoreSwitchOverString(MethodNode mth, SwitchRegion switchRegion) { + try { + InsnNode swInsn = BlockUtils.getLastInsnWithType(switchRegion.getHeader(), InsnType.SWITCH); + if (swInsn == null) { + return false; + } + RegisterArg strArg = getStrHashCodeArg(swInsn.getArg(0)); + if (strArg == null) { + return false; + } + int casesCount = switchRegion.getCases().size(); + SSAVar strVar = strArg.getSVar(); + if (strVar.getUseCount() - 1 < casesCount) { + // one 'hashCode' invoke and at least one 'equals' per case + return false; + } + // quick checks done, start collecting data to create a new switch region + Map strEqInsns = collectEqualsInsns(mth, strVar); + if (strEqInsns.size() < casesCount) { + return false; + } + SwitchData switchData = new SwitchData(mth, switchRegion); + switchData.setStrEqInsns(strEqInsns); + switchData.setCases(new ArrayList<>(strEqInsns.size())); + for (SwitchRegion.CaseInfo swCaseInfo : switchRegion.getCases()) { + if (!processCase(switchData, swCaseInfo)) { + mth.addWarnComment("Failed to restore switch over string. Please report as a decompilation issue"); + return false; + } + } + // match remapping var to collect code from second switch + if (!mergeWithCode(switchData)) { + mth.addWarnComment("Failed to restore switch over string. Please report as a decompilation issue"); + return false; + } + // all checks passed, replace with new switch + IRegion parentRegion = switchRegion.getParent(); + SwitchRegion replaceRegion = new SwitchRegion(parentRegion, switchRegion.getHeader()); + replaceRegion.addDefaultCase(switchData.getDefaultCode()); + for (CaseData caseData : switchData.getCases()) { + replaceRegion.addCase(Collections.unmodifiableList(caseData.getStrValues()), caseData.getCode()); + } + if (!parentRegion.replaceSubBlock(switchRegion, replaceRegion)) { + mth.addWarnComment("Failed to restore switch over string. Please report as a decompilation issue"); + return false; + } + // replace confirmed, remove original code + markCodeForRemoval(switchData); + // use string arg directly in switch + swInsn.replaceArg(swInsn.getArg(0), strArg.duplicate()); + return true; + } catch (Throwable e) { + mth.addWarnComment("Failed to restore switch over string. Please report as a decompilation issue", e); + return false; + } + } + + private static void markCodeForRemoval(SwitchData switchData) { + MethodNode mth = switchData.getMth(); + try { + switchData.getToRemove().forEach(i -> i.add(AFlag.REMOVE)); + SwitchRegion codeSwitch = switchData.getCodeSwitch(); + if (codeSwitch != null) { + IRegion parentRegion = switchData.getSwitchRegion().getParent(); + parentRegion.getSubBlocks().remove(codeSwitch); + codeSwitch.getHeader().add(AFlag.REMOVE); + } + RegisterArg numArg = switchData.getNumArg(); + if (numArg != null) { + for (SSAVar ssaVar : numArg.getSVar().getCodeVar().getSsaVars()) { + InsnNode assignInsn = ssaVar.getAssignInsn(); + if (assignInsn != null) { + assignInsn.add(AFlag.REMOVE); + } + for (RegisterArg useArg : ssaVar.getUseList()) { + InsnNode parentInsn = useArg.getParentInsn(); + if (parentInsn != null) { + parentInsn.add(AFlag.REMOVE); + } + } + mth.removeSVar(ssaVar); + } + } + InsnRemover.removeAllMarked(mth); + } catch (Throwable e) { + mth.addWarnComment("Failed to clean up code after switch over string restore", e); + } + } + + private boolean mergeWithCode(SwitchData switchData) { + List cases = switchData.getCases(); + // search index assign in cases code + RegisterArg numArg = null; + int extracted = 0; + for (CaseData caseData : cases) { + IContainer container = caseData.getCode(); + List insns = RegionUtils.collectInsns(switchData.getMth(), container); + insns.removeIf(i -> i.getType() == InsnType.BREAK); + if (insns.size() != 1) { + continue; + } + InsnNode numInsn = insns.get(0); + if (numInsn.getArgsCount() == 1) { + Object constVal = InsnUtils.getConstValueByArg(switchData.getMth().root(), numInsn.getArg(0)); + if (constVal instanceof LiteralArg) { + if (numArg == null) { + numArg = numInsn.getResult(); + } else { + if (!numArg.sameCodeVar(numInsn.getResult())) { + return false; + } + } + int num = (int) ((LiteralArg) constVal).getLiteral(); + caseData.setCodeNum(num); + extracted++; + } + } + } + if (extracted == 0) { + // nothing to merge, code already inside first switch cases + return true; + } + if (extracted != cases.size()) { + return false; + } + // TODO: additional checks for found index numbers + cases.sort(Comparator.comparingInt(CaseData::getCodeNum)); + + // extract complete, second switch on 'numArg' should be the next region + IContainer nextContainer = RegionUtils.getNextContainer(switchData.getMth(), switchData.getSwitchRegion()); + if (!(nextContainer instanceof SwitchRegion)) { + return false; + } + SwitchRegion codeSwitch = (SwitchRegion) nextContainer; + InsnNode swInsn = BlockUtils.getLastInsnWithType(codeSwitch.getHeader(), InsnType.SWITCH); + if (swInsn == null || !swInsn.getArg(0).isSameCodeVar(numArg)) { + return false; + } + Map casesMap = new HashMap<>(cases.size()); + for (CaseData caseData : cases) { + CaseData prev = casesMap.put(caseData.getCodeNum(), caseData); + if (prev != null) { + return false; + } + RegionUtils.visitBlocks(switchData.getMth(), caseData.getCode(), + block -> switchData.getToRemove().add(block)); + } + + IContainer defaultContainer = null; + for (SwitchRegion.CaseInfo caseInfo : codeSwitch.getCases()) { + CaseData prevCase = null; + for (Object key : caseInfo.getKeys()) { + if (key instanceof Integer) { + Integer intKey = (Integer) key; + CaseData caseData = casesMap.get(intKey); + if (caseData == null) { + return false; + } + if (prevCase == null) { + caseData.setCode(caseInfo.getContainer()); + prevCase = caseData; + } else { + // merge cases + prevCase.getStrValues().addAll(caseData.getStrValues()); + caseData.setCodeNum(-1); + } + } else if (key == SwitchRegion.DEFAULT_CASE_KEY) { + defaultContainer = caseInfo.getContainer(); + } else { + return false; + } + } + } + cases.removeIf(c -> c.getCodeNum() == -1); + + switchData.setDefaultCode(defaultContainer); + switchData.setCodeSwitch(codeSwitch); + switchData.setNumArg(numArg); + return true; + } + + private static Map collectEqualsInsns(MethodNode mth, SSAVar strVar) { + Map map = new IdentityHashMap<>(strVar.getUseCount() - 1); + for (RegisterArg useReg : strVar.getUseList()) { + InsnNode parentInsn = useReg.getParentInsn(); + if (parentInsn != null && parentInsn.getType() == InsnType.INVOKE) { + InvokeNode inv = (InvokeNode) parentInsn; + if (inv.getCallMth().getRawFullId().equals("java.lang.String.equals(Ljava/lang/Object;)Z")) { + InsnArg strArg = inv.getArg(1); + Object strValue = InsnUtils.getConstValueByArg(mth.root(), strArg); + if (strValue instanceof String) { + map.put(parentInsn, (String) strValue); + } + } + } + } + return map; + } + + private boolean processCase(SwitchData switchData, SwitchRegion.CaseInfo caseInfo) { + AtomicBoolean fail = new AtomicBoolean(false); + RegionUtils.visitRegions(switchData.getMth(), caseInfo.getContainer(), region -> { + if (fail.get()) { + return false; + } + if (region instanceof IfRegion) { + CaseData caseData = fillCaseData((IfRegion) region, switchData); + if (caseData == null) { + fail.set(true); + return false; + } + switchData.getCases().add(caseData); + } + return true; + }); + return !fail.get(); + } + + private @Nullable CaseData fillCaseData(IfRegion ifRegion, SwitchData switchData) { + IfCondition condition = Objects.requireNonNull(ifRegion.getCondition()); + boolean neg = false; + if (condition.getMode() == IfCondition.Mode.NOT) { + condition = condition.getArgs().get(0); + neg = true; + } + String str = null; + if (condition.isCompare()) { + IfNode ifInsn = condition.getCompare().getInsn(); + InsnArg firstArg = ifInsn.getArg(0); + if (firstArg.isInsnWrap()) { + str = switchData.getStrEqInsns().get(((InsnWrapArg) firstArg).getWrapInsn()); + } + if (ifInsn.getOp() == IfOp.NE && ifInsn.getArg(1).isTrue()) { + neg = true; + } + if (str != null) { + switchData.getToRemove().add(ifInsn); + switchData.getToRemove().addAll(ifRegion.getConditionBlocks()); + } + } + if (str == null) { + return null; + } + CaseData caseData = new CaseData(); + caseData.getStrValues().add(str); + caseData.setCode(neg ? ifRegion.getElseRegion() : ifRegion.getThenRegion()); + return caseData; + } + + private @Nullable RegisterArg getStrHashCodeArg(InsnArg arg) { + if (arg.isRegister()) { + return getStrFromInsn(((RegisterArg) arg).getAssignInsn()); + } + if (arg.isInsnWrap()) { + return getStrFromInsn(((InsnWrapArg) arg).getWrapInsn()); + } + return null; + } + + private @Nullable RegisterArg getStrFromInsn(@Nullable InsnNode insn) { + if (insn == null || insn.getType() != InsnType.INVOKE) { + return null; + } + InvokeNode invInsn = (InvokeNode) insn; + MethodInfo callMth = invInsn.getCallMth(); + if (!callMth.getRawFullId().equals("java.lang.String.hashCode()I")) { + return null; + } + InsnArg arg = invInsn.getInstanceArg(); + if (arg == null || !arg.isRegister()) { + return null; + } + return (RegisterArg) arg; + } + + private static final class SwitchData { + private final MethodNode mth; + private final SwitchRegion switchRegion; + private final List toRemove = new ArrayList<>(); + private Map strEqInsns; + private List cases; + private IContainer defaultCode; + private SwitchRegion codeSwitch; + private RegisterArg numArg; + + private SwitchData(MethodNode mth, SwitchRegion switchRegion) { + this.mth = mth; + this.switchRegion = switchRegion; + } + + public List getCases() { + return cases; + } + + public void setCases(List cases) { + this.cases = cases; + } + + public IContainer getDefaultCode() { + return defaultCode; + } + + public void setDefaultCode(IContainer defaultCode) { + this.defaultCode = defaultCode; + } + + public MethodNode getMth() { + return mth; + } + + public Map getStrEqInsns() { + return strEqInsns; + } + + public void setStrEqInsns(Map strEqInsns) { + this.strEqInsns = strEqInsns; + } + + public SwitchRegion getSwitchRegion() { + return switchRegion; + } + + public List getToRemove() { + return toRemove; + } + + public SwitchRegion getCodeSwitch() { + return codeSwitch; + } + + public void setCodeSwitch(SwitchRegion codeSwitch) { + this.codeSwitch = codeSwitch; + } + + public RegisterArg getNumArg() { + return numArg; + } + + public void setNumArg(RegisterArg numArg) { + this.numArg = numArg; + } + } + + private static final class CaseData { + private final List strValues = new ArrayList<>(); + private IContainer code = null; + private int codeNum = -1; + + public List getStrValues() { + return strValues; + } + + public IContainer getCode() { + return code; + } + + public void setCode(IContainer code) { + this.code = code; + } + + public int getCodeNum() { + return codeNum; + } + + public void setCodeNum(int codeNum) { + this.codeNum = codeNum; + } + + @Override + public String toString() { + return "CaseData{" + strValues + '}'; + } + } +} diff --git a/jadx-core/src/main/java/jadx/core/utils/InsnRemover.java b/jadx-core/src/main/java/jadx/core/utils/InsnRemover.java index e6ff2f46..1e2b1003 100644 --- a/jadx-core/src/main/java/jadx/core/utils/InsnRemover.java +++ b/jadx-core/src/main/java/jadx/core/utils/InsnRemover.java @@ -189,7 +189,10 @@ public class InsnRemover { } } - public static void remove(MethodNode mth, InsnNode insn) { + public static void remove(MethodNode mth, @Nullable InsnNode insn) { + if (insn == null) { + return; + } if (insn.contains(AFlag.WRAPPED)) { unbindInsn(mth, insn); return; diff --git a/jadx-core/src/main/java/jadx/core/utils/RegionUtils.java b/jadx-core/src/main/java/jadx/core/utils/RegionUtils.java index e2cb14f3..5ed98d14 100644 --- a/jadx-core/src/main/java/jadx/core/utils/RegionUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/RegionUtils.java @@ -5,6 +5,7 @@ import java.util.Collections; import java.util.List; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Predicate; import org.jetbrains.annotations.Nullable; @@ -263,6 +264,12 @@ public class RegionUtils { throw new JadxRuntimeException(unknownContainerType(container)); } + public static List collectInsns(MethodNode mth, IContainer container) { + List list = new ArrayList<>(); + visitBlocks(mth, container, block -> list.addAll(block.getInstructions())); + return list; + } + public static boolean isEmpty(IContainer container) { return !notEmpty(container); } @@ -509,4 +516,23 @@ public class RegionUtils { } }); } + + public static void visitRegions(MethodNode mth, IContainer container, Predicate visitor) { + DepthRegionTraversal.traverse(mth, container, new AbstractRegionVisitor() { + @Override + public boolean enterRegion(MethodNode mth, IRegion region) { + return visitor.test(region); + } + }); + } + + public static @Nullable IContainer getNextContainer(MethodNode mth, IRegion region) { + IRegion parent = region.getParent(); + List subBlocks = parent.getSubBlocks(); + int index = subBlocks.indexOf(region); + if (index == -1 || index + 1 >= subBlocks.size()) { + return null; + } + return subBlocks.get(index + 1); + } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchOverStrings.java b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchOverStrings.java new file mode 100644 index 00000000..b0695b97 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchOverStrings.java @@ -0,0 +1,52 @@ +package jadx.tests.integration.switches; + +import org.junit.jupiter.api.Test; + +import jadx.tests.api.IntegrationTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestSwitchOverStrings extends IntegrationTest { + + /** + * Strings 'frewhyh', 'phgafkp' and 'ucguedt' have same hash code. + */ + public static class TestCls { + + public int test(String str) { + switch (str) { + case "frewhyh": + return 1; + case "phgafkp": + return 2; + case "test": + case "test2": + return 3; + case "other": + return 4; + default: + return 0; + } + } + + public void check() { + assertThat(test("frewhyh")).isEqualTo(1); + assertThat(test("phgafkp")).isEqualTo(2); + assertThat(test("test")).isEqualTo(3); + assertThat(test("test2")).isEqualTo(3); + assertThat(test("other")).isEqualTo(4); + assertThat(test("unknown")).isEqualTo(0); + assertThat(test("ucguedt")).isEqualTo(0); + } + } + + @Test + public void test() { + assertThat(getClassNode(TestCls.class)) + .code() + .doesNotContain("case -603257287:") + .doesNotContain("c = ") + .containsOne("case \"frewhyh\":") + .countString(5, "return "); + } +} 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 9db11960..8a92b9b4 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java @@ -466,6 +466,10 @@ public class JadxSettings extends JadxCLIArgs { this.extractFinally = extractFinally; } + public void setRestoreSwitchOverString(boolean restoreSwitchOverString) { + this.restoreSwitchOverString = restoreSwitchOverString; + } + public void setFsCaseSensitive(boolean fsCaseSensitive) { this.fsCaseSensitive = fsCaseSensitive; } diff --git a/jadx-gui/src/main/java/jadx/gui/settings/ui/JadxSettingsWindow.java b/jadx-gui/src/main/java/jadx/gui/settings/ui/JadxSettingsWindow.java index 2b9d2d5c..e7d4255e 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/ui/JadxSettingsWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/ui/JadxSettingsWindow.java @@ -550,6 +550,13 @@ public class JadxSettingsWindow extends JDialog { needReload(); }); + JCheckBox restoreSwitchOverString = new JCheckBox(); + restoreSwitchOverString.setSelected(settings.isRestoreSwitchOverString()); + restoreSwitchOverString.addItemListener(e -> { + settings.setRestoreSwitchOverString(e.getStateChange() == ItemEvent.SELECTED); + needReload(); + }); + JCheckBox fsCaseSensitive = new JCheckBox(); fsCaseSensitive.setSelected(settings.isFsCaseSensitive()); fsCaseSensitive.addItemListener(e -> { @@ -595,6 +602,7 @@ public class JadxSettingsWindow extends JDialog { other.addRow(NLS.str("preferences.inlineKotlinLambdas"), inlineKotlinLambdas); other.addRow(NLS.str("preferences.moveInnerClasses"), moveInnerClasses); other.addRow(NLS.str("preferences.extractFinally"), extractFinally); + other.addRow(NLS.str("preferences.restoreSwitchOverString"), restoreSwitchOverString); other.addRow(NLS.str("preferences.fsCaseSensitive"), fsCaseSensitive); other.addRow(NLS.str("preferences.useDx"), useDx); other.addRow(NLS.str("preferences.skipResourcesDecode"), resourceDecode); 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 45f2518e..f087a9c9 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -206,6 +206,7 @@ preferences.inlineMethods=Inline-Methoden #preferences.inlineKotlinLambdas=Allow to inline Kotlin Lambdas #preferences.moveInnerClasses=Move inner classes into parent #preferences.extractFinally=Extract finally block +#preferences.restoreSwitchOverString=Restore switch over string preferences.fsCaseSensitive=Dateisystem unterscheidet zwischen Groß/Kleinschreibung preferences.skipResourcesDecode=Keine Ressourcen dekodieren preferences.useKotlinMethodsForVarNames=Kotlin-Methoden für die Umbenennung von Variablen verwenden 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 5a943c52..05df7cdc 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -206,6 +206,7 @@ preferences.inlineMethods=Inline methods preferences.inlineKotlinLambdas=Allow to inline Kotlin Lambdas preferences.moveInnerClasses=Move inner classes into parent preferences.extractFinally=Extract finally block +preferences.restoreSwitchOverString=Restore switch over string preferences.fsCaseSensitive=File system is case-sensitive preferences.skipResourcesDecode=Don't decode resources preferences.useKotlinMethodsForVarNames=Use kotlin methods for variables rename 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 4f38bea7..8c3b9330 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -206,6 +206,7 @@ preferences.replaceConsts=Reemplazar constantes #preferences.inlineKotlinLambdas=Allow to inline Kotlin Lambdas #preferences.moveInnerClasses=Move inner classes into parent #preferences.extractFinally=Extract finally block +#preferences.restoreSwitchOverString=Restore switch over string #preferences.fsCaseSensitive= preferences.skipResourcesDecode=No descodificar recursos #preferences.useKotlinMethodsForVarNames=Use kotlin methods for variables rename diff --git a/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties b/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties index af9b2f62..967355eb 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties @@ -206,6 +206,7 @@ preferences.inlineMethods=Inline metode preferences.inlineKotlinLambdas=Izinkan untuk menggantikan Lambdas Kotlin preferences.moveInnerClasses=Pindahkan kelas dalam ke kelas induk preferences.extractFinally=Ekstrak blok finally +#preferences.restoreSwitchOverString=Restore switch over string preferences.fsCaseSensitive=File sistem bersifat sensitif huruf besar-kecil preferences.skipResourcesDecode=Jangan dekode sumber daya preferences.useKotlinMethodsForVarNames=Gunakan metode Kotlin untuk mengganti nama variabel 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 9ce6db82..1c4a08b6 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -206,6 +206,7 @@ preferences.inlineMethods=인라인 메서드 #preferences.inlineKotlinLambdas=Allow to inline Kotlin Lambdas #preferences.moveInnerClasses=Move inner classes into parent preferences.extractFinally=finally 블록 추출 +#preferences.restoreSwitchOverString=Restore switch over string preferences.fsCaseSensitive=파일 시스템 대소문자 구별 preferences.skipResourcesDecode=리소스 디코딩 하지 않기 preferences.useKotlinMethodsForVarNames=변수 이름 바꾸기에 kotlin 메서드 사용 diff --git a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties index 767b2944..dc1bee6c 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties @@ -206,6 +206,7 @@ preferences.inlineMethods=Métodos de uma linha #preferences.inlineKotlinLambdas=Allow to inline Kotlin Lambdas #preferences.moveInnerClasses=Move inner classes into parent preferences.extractFinally=Extrair blocos finally +#preferences.restoreSwitchOverString=Restore switch over string preferences.fsCaseSensitive=Sistema de arquivo diferencia maiúsculas de minúsculas preferences.skipResourcesDecode=Não decodificar recursos preferences.useKotlinMethodsForVarNames=Usar métodos do kotlin para renomear variáveis diff --git a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties index 17f08794..388644f3 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties @@ -206,6 +206,7 @@ preferences.inlineMethods=Объединять методы preferences.inlineKotlinLambdas=Разрешить инлайнить Kotlin лямбды preferences.moveInnerClasses=Помещать вложенные классы внутрь родительских preferences.extractFinally=Вычленять finally блоки +#preferences.restoreSwitchOverString=Restore switch over string preferences.fsCaseSensitive=Учитывать регистр в файловой системе preferences.skipResourcesDecode=Не декодировать ресурсы preferences.useKotlinMethodsForVarNames=Kotlin методы как имена полей 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 520a4e35..eec59065 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -206,6 +206,7 @@ preferences.inlineMethods=内联方法 preferences.inlineKotlinLambdas=允许内联Kotlin Lambda preferences.moveInnerClasses=将内部类移到父类 preferences.extractFinally=提取finally块 +#preferences.restoreSwitchOverString=Restore switch over string preferences.fsCaseSensitive=文件系统区分大小写 preferences.skipResourcesDecode=不反编译资源文件 preferences.useKotlinMethodsForVarNames=使用Kotlin方法来重命名变量 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 57f57740..c9649f9f 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties @@ -206,6 +206,7 @@ preferences.inlineMethods=內嵌方式 preferences.inlineKotlinLambdas=允許內嵌 Kotlin 匿名函式 preferences.moveInnerClasses=將內部類別移動至父類別 preferences.extractFinally=擷取 finally 區塊 +#preferences.restoreSwitchOverString=Restore switch over string preferences.fsCaseSensitive=檔案系統區分大小寫 preferences.skipResourcesDecode=不要為資源解碼 preferences.useKotlinMethodsForVarNames=使用 Kotlin 方法來為變數重新命名