feat: support restore of switch over string (basic case)(#2288)

This commit is contained in:
Skylot 2024-09-27 21:07:07 +01:00
parent a7649dda7a
commit 0b225238fb
No known key found for this signature in database
GPG Key ID: 47A4975761262B6A
26 changed files with 739 additions and 101 deletions

180
README.md
View File

@ -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

View File

@ -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();

View File

@ -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;
}

View File

@ -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

View File

@ -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());

View File

@ -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));
}

View File

@ -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<>();

View File

@ -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 + '}';
}
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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 + '}';
}
}
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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 ");
}
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 메서드 사용

View File

@ -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

View File

@ -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 методы как имена полей

View File

@ -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方法来重命名变量

View File

@ -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 方法來為變數重新命名