diff --git a/jadx-core/src/main/java/jadx/api/plugins/options/impl/BaseOptionsParser.java b/jadx-core/src/main/java/jadx/api/plugins/options/impl/BaseOptionsParser.java index 912a51bf3..b109b5b70 100644 --- a/jadx-core/src/main/java/jadx/api/plugins/options/impl/BaseOptionsParser.java +++ b/jadx-core/src/main/java/jadx/api/plugins/options/impl/BaseOptionsParser.java @@ -6,6 +6,10 @@ import java.util.function.Function; import jadx.api.plugins.options.JadxPluginOptions; +/** + * Prefer {@link BasePluginOptionsBuilder} as a better way to init and parse options + */ +@Deprecated public abstract class BaseOptionsParser implements JadxPluginOptions { protected Map options; diff --git a/jadx-core/src/main/java/jadx/api/plugins/options/impl/BasePluginOptionsBuilder.java b/jadx-core/src/main/java/jadx/api/plugins/options/impl/BasePluginOptionsBuilder.java new file mode 100644 index 000000000..3718228f5 --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/plugins/options/impl/BasePluginOptionsBuilder.java @@ -0,0 +1,242 @@ +package jadx.api.plugins.options.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.jetbrains.annotations.Nullable; + +import jadx.api.plugins.options.JadxPluginOptions; +import jadx.api.plugins.options.OptionDescription; +import jadx.api.plugins.options.OptionFlag; +import jadx.api.plugins.options.OptionType; + +/** + * Base class for {@link JadxPluginOptions} implementation + *

+ * Override {@link BasePluginOptionsBuilder#registerOptions()} method + * and use *option methods to add option info. + */ +@SuppressWarnings("unused") +public abstract class BasePluginOptionsBuilder implements JadxPluginOptions { + + private final List> options = new ArrayList<>(); + + public abstract void registerOptions(); + + public BasePluginOptionsBuilder() { + registerOptions(); + for (OptionData option : options) { + option.validate(); + } + } + + public OptionBuilder option(String name) { + return addOption(new OptionData<>(name)); + } + + public OptionBuilder option(String name, Class optionType) { + return addOption(new OptionData<>(name)); + } + + public OptionBuilder boolOption(String name) { + return addOption( + new OptionData(name) + .type(OptionType.BOOLEAN) + .values(Arrays.asList(Boolean.TRUE, Boolean.FALSE)) + .formatter(b -> b ? "yes" : "no") + .parser(val -> parseBoolOption(name, val))); + } + + public OptionBuilder strOption(String name) { + return addOption( + new OptionData(name) + .type(OptionType.STRING) + .formatter(v -> v) + .parser(v -> v)); + } + + public > OptionBuilder enumOption(String name, E[] values, Function valueOf) { + return addOption( + new OptionData(name) + .type(OptionType.STRING) + .values(Arrays.asList(values)) + .formatter(v -> v.name().toLowerCase(Locale.ROOT)) + .parser(v -> valueOf.apply(v.toUpperCase(Locale.ROOT)))); + } + + @Override + public void setOptions(Map map) { + for (OptionData option : options) { + parseOption(option, map.get(option.name)); + } + } + + @Override + public List getOptionsDescriptions() { + return Collections.unmodifiableList(options); + } + + private static void parseOption(OptionData option, @Nullable String value) { + T parsedValue; + if (value == null) { + parsedValue = option.defaultValue; + } else { + try { + parsedValue = option.getParser().apply(value); + } catch (Exception e) { + throw new RuntimeException("Parse failed for option: " + option.name + ", value: " + value, e); + } + } + try { + option.getSetter().accept(parsedValue); + } catch (Exception e) { + throw new RuntimeException("Setter invoke failed for option: " + option.name + ", value: " + parsedValue, e); + } + } + + private static boolean parseBoolOption(String name, String val) { + String valLower = val.trim().toLowerCase(Locale.ROOT); + if (valLower.equals("yes") || valLower.equals("true")) { + return true; + } + if (valLower.equals("no") || valLower.equals("false")) { + return false; + } + throw new IllegalArgumentException("Unknown value '" + val + "' for option '" + name + "', expect: 'yes' or 'no'"); + } + + private OptionBuilder addOption(OptionBuilder optionData) { + this.options.add((OptionData) optionData); + return optionData; + } + + protected static class OptionData implements OptionDescription, OptionBuilder { + private final String name; + private String desc; + private List values = Collections.emptyList(); + private OptionType type = OptionType.STRING; + private Set flags = EnumSet.noneOf(OptionFlag.class); + private Function parser; + private Function formatter; + private Consumer setter; + private T defaultValue; + + public OptionData(String name) { + this.name = name; + } + + @Override + public String name() { + return name; + } + + @Override + public String description() { + return desc; + } + + @Override + public List values() { + return values.stream().map(formatter).collect(Collectors.toList()); + } + + @Override + public @Nullable String defaultValue() { + return formatter.apply(defaultValue); + } + + @Override + public OptionType getType() { + return type; + } + + @Override + public Set getFlags() { + return flags; + } + + @Override + public OptionBuilder description(String desc) { + this.desc = desc; + return this; + } + + @Override + public OptionBuilder defaultValue(@Nullable T defValue) { + this.defaultValue = defValue; + return this; + } + + @Override + public OptionBuilder parser(Function parser) { + this.parser = parser; + return this; + } + + @Override + public OptionBuilder formatter(Function formatter) { + this.formatter = formatter; + return this; + } + + @Override + public OptionBuilder setter(Consumer setter) { + this.setter = setter; + return this; + } + + @Override + public OptionBuilder type(OptionType optionType) { + this.type = optionType; + return this; + } + + @Override + public OptionBuilder flags(OptionFlag... flags) { + this.flags = EnumSet.copyOf(Arrays.asList(flags)); + return this; + } + + @Override + public OptionBuilder values(List values) { + this.values = values; + return this; + } + + public Function getParser() { + return parser; + } + + public Function getFormatter() { + return formatter; + } + + public Consumer getSetter() { + return setter; + } + + public void validate() { + if (desc == null || desc.isEmpty()) { + throw new IllegalArgumentException("Description should be set for option: " + name); + } + if (parser == null) { + throw new IllegalArgumentException("Parser should be set for option: " + name); + } + if (formatter == null) { + throw new IllegalArgumentException("Formatter should be set for option: " + name); + } + if (setter == null) { + throw new IllegalArgumentException("Setter should be set for option: " + name); + } + } + } +} diff --git a/jadx-core/src/main/java/jadx/api/plugins/options/impl/OptionBuilder.java b/jadx-core/src/main/java/jadx/api/plugins/options/impl/OptionBuilder.java new file mode 100644 index 000000000..372f7e630 --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/plugins/options/impl/OptionBuilder.java @@ -0,0 +1,42 @@ +package jadx.api.plugins.options.impl; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + +import jadx.api.plugins.options.OptionFlag; +import jadx.api.plugins.options.OptionType; + +public interface OptionBuilder { + + /** + * Option description (required) + */ + OptionBuilder description(String desc); + + OptionBuilder defaultValue(T defValue); + + /** + * Function to parse input string into option value (required) + */ + OptionBuilder parser(Function parser); + + /** + * Function to format option value into string for build help (required) + */ + OptionBuilder formatter(Function formatter); + + /** + * Function to save/apply parsed option value (required) + */ + OptionBuilder setter(Consumer setter); + + /** + * Possible option values + */ + OptionBuilder values(List values); + + OptionBuilder type(OptionType optionType); + + OptionBuilder flags(OptionFlag... flags); +} diff --git a/jadx-core/src/main/java/jadx/core/utils/ListUtils.java b/jadx-core/src/main/java/jadx/core/utils/ListUtils.java index fce3fc92e..c4a50efc6 100644 --- a/jadx-core/src/main/java/jadx/core/utils/ListUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/ListUtils.java @@ -1,6 +1,7 @@ package jadx.core.utils; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; @@ -64,6 +65,13 @@ public class ListUtils { return new ArrayList<>(new LinkedHashSet<>(list)); } + public static List concat(T first, T[] values) { + List list = new ArrayList<>(1 + values.length); + list.add(first); + list.addAll(Arrays.asList(values)); + return list; + } + /** * Replace old element to new one. * Support null and empty immutable list (created by Collections.emptyList()) @@ -175,4 +183,5 @@ public class ListUtils { } return false; } + } diff --git a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputOptions.java b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputOptions.java index 12da31ab1..d5717982d 100644 --- a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputOptions.java +++ b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputOptions.java @@ -1,29 +1,17 @@ package jadx.plugins.input.dex; -import java.util.Collections; -import java.util.List; +import jadx.api.plugins.options.impl.BasePluginOptionsBuilder; -import jadx.api.plugins.options.OptionDescription; -import jadx.api.plugins.options.impl.BaseOptionsParser; -import jadx.api.plugins.options.impl.JadxOptionDescription; +public class DexInputOptions extends BasePluginOptionsBuilder { -public class DexInputOptions extends BaseOptionsParser { - - private static final String VERIFY_CHECKSUM_OPT = DexInputPlugin.PLUGIN_ID + ".verify-checksum"; - - private boolean verifyChecksum = true; + private boolean verifyChecksum; @Override - public void parseOptions() { - verifyChecksum = getBooleanOption(VERIFY_CHECKSUM_OPT, true); - } - - public List getOptionsDescriptions() { - return Collections.singletonList( - JadxOptionDescription.booleanOption( - VERIFY_CHECKSUM_OPT, - "verify dex file checksum before load", - true)); + public void registerOptions() { + boolOption(DexInputPlugin.PLUGIN_ID + ".verify-checksum") + .description("verify dex file checksum before load") + .defaultValue(true) + .setter(v -> verifyChecksum = v); } public boolean isVerifyChecksum() { diff --git a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertOptions.java b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertOptions.java index 516728795..819db8b1a 100644 --- a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertOptions.java +++ b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertOptions.java @@ -1,44 +1,29 @@ package jadx.plugins.input.javaconvert; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; +import jadx.api.plugins.options.impl.BasePluginOptionsBuilder; -import jadx.api.plugins.options.OptionDescription; -import jadx.api.plugins.options.impl.BaseOptionsParser; -import jadx.api.plugins.options.impl.JadxOptionDescription; -import jadx.core.utils.files.FileUtils; +import static jadx.plugins.input.javaconvert.JavaConvertPlugin.PLUGIN_ID; -public class JavaConvertOptions extends BaseOptionsParser { - - private static final String MODE_OPT = JavaConvertPlugin.PLUGIN_ID + ".mode"; - private static final String D8_DESUGAR_OPT = JavaConvertPlugin.PLUGIN_ID + ".d8-desugar"; +public class JavaConvertOptions extends BasePluginOptionsBuilder { public enum Mode { DX, D8, BOTH } - private Mode mode = Mode.BOTH; - private boolean d8Desugar = false; + private Mode mode; + private boolean d8Desugar; @Override - public void parseOptions() { - mode = getOption(MODE_OPT, name -> Mode.valueOf(name.toUpperCase(Locale.ROOT)), Mode.BOTH); - d8Desugar = getBooleanOption(D8_DESUGAR_OPT, false); - } + public void registerOptions() { + enumOption(PLUGIN_ID + ".mode", Mode.values(), Mode::valueOf) + .description("convert mode") + .defaultValue(Mode.BOTH) + .setter(v -> mode = v); - @Override - public List getOptionsDescriptions() { - return Arrays.asList( - new JadxOptionDescription( - MODE_OPT, - "convert mode", - "both", - Arrays.asList("dx", "d8", "both")), - JadxOptionDescription.booleanOption( - D8_DESUGAR_OPT, - "use desugar in d8", - false)); + boolOption(PLUGIN_ID + ".d8-desugar") + .description("use desugar in d8") + .defaultValue(false) + .setter(v -> d8Desugar = v); } public Mode getMode() { @@ -48,8 +33,4 @@ public class JavaConvertOptions extends BaseOptionsParser { public boolean isD8Desugar() { return d8Desugar; } - - public String getOptionsHash() { - return FileUtils.md5Sum(mode + ":" + d8Desugar); - } } diff --git a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java index 146fe6b70..5ee72aaf2 100644 --- a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java +++ b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java @@ -32,7 +32,6 @@ public class JavaConvertPlugin implements JadxPlugin, JadxCodeInput { public void init(JadxPluginContext context) { context.registerOptions(options); context.addCodeInput(this); - context.registerInputsHashSupplier(options::getOptionsHash); } @Override diff --git a/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/KotlinMetadataOptions.kt b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/KotlinMetadataOptions.kt index 3786c5781..ccaa9dbb8 100644 --- a/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/KotlinMetadataOptions.kt +++ b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/KotlinMetadataOptions.kt @@ -1,11 +1,9 @@ package jadx.plugins.kotlin.metadata -import jadx.api.plugins.options.OptionDescription -import jadx.api.plugins.options.impl.BaseOptionsParser -import jadx.api.plugins.options.impl.JadxOptionDescription.booleanOption +import jadx.api.plugins.options.impl.BasePluginOptionsBuilder import jadx.plugins.kotlin.metadata.KotlinMetadataPlugin.Companion.PLUGIN_ID -class KotlinMetadataOptions : BaseOptionsParser() { +class KotlinMetadataOptions : BasePluginOptionsBuilder() { var isClassAlias: Boolean = true private set var isMethodArgs: Boolean = true @@ -21,26 +19,41 @@ class KotlinMetadataOptions : BaseOptionsParser() { var isGetters: Boolean = true private set - override fun parseOptions() { - isClassAlias = getBooleanOption(CLASS_ALIAS_OPT, true) - isMethodArgs = getBooleanOption(METHOD_ARGS_OPT, true) - isFields = getBooleanOption(FIELDS_OPT, true) - isCompanion = getBooleanOption(COMPANION_OPT, true) - isDataClass = getBooleanOption(DATA_CLASS_OPT, true) - isToString = getBooleanOption(TO_STRING_OPT, true) - isGetters = getBooleanOption(GETTERS_OPT, true) - } + override fun registerOptions() { + boolOption(CLASS_ALIAS_OPT) + .description("rename class alias") + .defaultValue(true) + .setter { isClassAlias = it } - override fun getOptionsDescriptions(): List { - return listOf( - booleanOption(CLASS_ALIAS_OPT, "rename class alias", true), - booleanOption(METHOD_ARGS_OPT, "rename function arguments", true), - booleanOption(FIELDS_OPT, "rename fields", true), - booleanOption(COMPANION_OPT, "rename companion object", true), - booleanOption(DATA_CLASS_OPT, "add data class modifier", true), - booleanOption(TO_STRING_OPT, "rename fields using toString", true), - booleanOption(GETTERS_OPT, "rename simple getters to field names", true), - ) + boolOption(METHOD_ARGS_OPT) + .description("rename function arguments") + .defaultValue(true) + .setter { isMethodArgs = it } + + boolOption(FIELDS_OPT) + .description("rename fields") + .defaultValue(true) + .setter { isFields = it } + + boolOption(COMPANION_OPT) + .description("rename companion object") + .defaultValue(true) + .setter { isCompanion = it } + + boolOption(DATA_CLASS_OPT) + .description("add data class modifier") + .defaultValue(true) + .setter { isDataClass = it } + + boolOption(TO_STRING_OPT) + .description("rename fields using toString") + .defaultValue(true) + .setter { isToString = it } + + boolOption(GETTERS_OPT) + .description("rename simple getters to field names") + .defaultValue(true) + .setter { isGetters = it } } fun isPreparePassNeeded(): Boolean { diff --git a/jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/RenameMappingsOptions.java b/jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/RenameMappingsOptions.java index a9b0f23c9..065ec4ab5 100644 --- a/jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/RenameMappingsOptions.java +++ b/jadx-plugins/jadx-rename-mappings/src/main/java/jadx/plugins/mappings/RenameMappingsOptions.java @@ -1,22 +1,18 @@ package jadx.plugins.mappings; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; import java.util.Locale; import org.jetbrains.annotations.Nullable; import net.fabricmc.mappingio.format.MappingFormat; -import jadx.api.plugins.options.OptionDescription; import jadx.api.plugins.options.OptionFlag; -import jadx.api.plugins.options.impl.BaseOptionsParser; -import jadx.api.plugins.options.impl.JadxOptionDescription; +import jadx.api.plugins.options.impl.BasePluginOptionsBuilder; +import jadx.core.utils.ListUtils; import static jadx.plugins.mappings.RenameMappingsPlugin.PLUGIN_ID; -public class RenameMappingsOptions extends BaseOptionsParser { +public class RenameMappingsOptions extends BasePluginOptionsBuilder { public static final String INVERT_OPT = PLUGIN_ID + ".invert"; public static final String FORMAT_OPT = PLUGIN_ID + ".format"; @@ -29,18 +25,21 @@ public class RenameMappingsOptions extends BaseOptionsParser { private @Nullable MappingFormat format = null; @Override - public void parseOptions() { - format = getOption(FORMAT_OPT, RenameMappingsOptions::parseMappingFormat, null); - invert = getBooleanOption(INVERT_OPT, false); - } + public void registerOptions() { + option(FORMAT_OPT, MappingFormat.class) + .description("mapping format") + .parser(RenameMappingsOptions::parseMappingFormat) + .formatter(v -> v == null ? "AUTO" : v.name()) + .values(ListUtils.concat(null, MappingFormat.values())) + .defaultValue(null) + .flags(OptionFlag.PER_PROJECT, OptionFlag.DISABLE_IN_GUI) + .setter(v -> format = v); - @Override - public List getOptionsDescriptions() { - return Arrays.asList( - new JadxOptionDescription(FORMAT_OPT, "mapping format", "auto", getMappingFormats()) - .withFlags(OptionFlag.PER_PROJECT, OptionFlag.DISABLE_IN_GUI), - JadxOptionDescription.booleanOption(INVERT_OPT, "invert mapping", false) - .withFlag(OptionFlag.PER_PROJECT)); + boolOption(INVERT_OPT) + .description("invert mapping on load") + .defaultValue(false) + .flags(OptionFlag.PER_PROJECT) + .setter(v -> invert = v); } private static MappingFormat parseMappingFormat(String name) { @@ -51,16 +50,7 @@ public class RenameMappingsOptions extends BaseOptionsParser { return MappingFormat.valueOf(upName); } - private static List getMappingFormats() { - List list = new ArrayList<>(); - list.add("auto"); - for (MappingFormat value : MappingFormat.values()) { - list.add(value.name()); - } - return list; - } - - public MappingFormat getFormat() { + public @Nullable MappingFormat getFormat() { return format; }