mirror of
https://github.com/skylot/jadx.git
synced 2024-11-22 20:29:51 +00:00
feat: support restore of switch over string (basic case)(#2288)
This commit is contained in:
parent
a7649dda7a
commit
0b225238fb
180
README.md
180
README.md
@ -91,95 +91,101 @@ commands (use '<command> --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<name>=<value>):
|
||||
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
|
||||
|
@ -228,7 +228,7 @@ public class JCommanderWrapper<T> {
|
||||
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<T> {
|
||||
return "\nPlugin options (-P<name>=<value>):" + 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<OptionDescription> 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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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<T extends IJadxAttribute> implements IJadxAttrType<T> {
|
||||
public static final AType<MethodOverrideAttr> METHOD_OVERRIDE = new AType<>();
|
||||
public static final AType<MethodTypeVarsAttr> METHOD_TYPE_VARS = new AType<>();
|
||||
public static final AType<AttrList<TryCatchBlockAttr>> TRY_BLOCKS_LIST = new AType<>();
|
||||
public static final AType<CodeFeaturesAttr> METHOD_CODE_FEATURES = new AType<>();
|
||||
|
||||
// region
|
||||
public static final AType<DeclareVariablesAttr> DECLARE_VARIABLES = new AType<>();
|
||||
|
@ -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<CodeFeature> codeFeatures = EnumSet.noneOf(CodeFeature.class);
|
||||
|
||||
public Set<CodeFeature> getCodeFeatures() {
|
||||
return codeFeatures;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AType<CodeFeaturesAttr> getAttrType() {
|
||||
return AType.METHOD_CODE_FEATURES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toAttrString() {
|
||||
return "CodeFeatures{" + codeFeatures + '}';
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 extends InsnArg> T copyCommonParams(T copy) {
|
||||
copy.copyAttributesFrom(this);
|
||||
copy.setParentInsn(parentInsn);
|
||||
|
@ -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<CaseInfo> getCases() {
|
||||
return cases;
|
||||
}
|
||||
|
@ -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<InsnNode, String> 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<CaseData> cases = switchData.getCases();
|
||||
// search index assign in cases code
|
||||
RegisterArg numArg = null;
|
||||
int extracted = 0;
|
||||
for (CaseData caseData : cases) {
|
||||
IContainer container = caseData.getCode();
|
||||
List<InsnNode> 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<Integer, CaseData> 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<InsnNode, String> collectEqualsInsns(MethodNode mth, SSAVar strVar) {
|
||||
Map<InsnNode, String> 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<IAttributeNode> toRemove = new ArrayList<>();
|
||||
private Map<InsnNode, String> strEqInsns;
|
||||
private List<CaseData> cases;
|
||||
private IContainer defaultCode;
|
||||
private SwitchRegion codeSwitch;
|
||||
private RegisterArg numArg;
|
||||
|
||||
private SwitchData(MethodNode mth, SwitchRegion switchRegion) {
|
||||
this.mth = mth;
|
||||
this.switchRegion = switchRegion;
|
||||
}
|
||||
|
||||
public List<CaseData> getCases() {
|
||||
return cases;
|
||||
}
|
||||
|
||||
public void setCases(List<CaseData> cases) {
|
||||
this.cases = cases;
|
||||
}
|
||||
|
||||
public IContainer getDefaultCode() {
|
||||
return defaultCode;
|
||||
}
|
||||
|
||||
public void setDefaultCode(IContainer defaultCode) {
|
||||
this.defaultCode = defaultCode;
|
||||
}
|
||||
|
||||
public MethodNode getMth() {
|
||||
return mth;
|
||||
}
|
||||
|
||||
public Map<InsnNode, String> getStrEqInsns() {
|
||||
return strEqInsns;
|
||||
}
|
||||
|
||||
public void setStrEqInsns(Map<InsnNode, String> strEqInsns) {
|
||||
this.strEqInsns = strEqInsns;
|
||||
}
|
||||
|
||||
public SwitchRegion getSwitchRegion() {
|
||||
return switchRegion;
|
||||
}
|
||||
|
||||
public List<IAttributeNode> 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<String> strValues = new ArrayList<>();
|
||||
private IContainer code = null;
|
||||
private int codeNum = -1;
|
||||
|
||||
public List<String> 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 + '}';
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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<InsnNode> collectInsns(MethodNode mth, IContainer container) {
|
||||
List<InsnNode> 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<IRegion> 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<IContainer> subBlocks = parent.getSubBlocks();
|
||||
int index = subBlocks.indexOf(region);
|
||||
if (index == -1 || index + 1 >= subBlocks.size()) {
|
||||
return null;
|
||||
}
|
||||
return subBlocks.get(index + 1);
|
||||
}
|
||||
}
|
||||
|
@ -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 ");
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 메서드 사용
|
||||
|
@ -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
|
||||
|
@ -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 методы как имена полей
|
||||
|
@ -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方法来重命名变量
|
||||
|
@ -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 方法來為變數重新命名
|
||||
|
Loading…
Reference in New Issue
Block a user